Skip to main content

bernoulli-types

bernoulli-py CI

bernoulli-types (import name bernoulli) is the reference implementation of a single error model for approximate answers. Every structure in it answers through a binary channel with two rates, the false-positive rate fpr and the false-negative rate fnr, and the package carries those rates through composition. Bloom filters, perfect-hash filters, retrieval structures, sketches, Miller-Rabin and noisy classifiers are all instances.

The structures are not new, and neither is most of the rate algebra. The package is a unification, written so that its claims can be executed and checked:

Other components follow Parker and McCluskey (1975) for signal probabilities through Boolean networks (doi:10.1109/T-C.1975.224279), Kirsch and Mitzenmacher (2006) for double hashing (doi:10.1007/11841036_42), Graf and Lemire (2020) for peeling-based retrieval (doi:10.1145/3376122), Cormode and Muthukrishnan (2005) for Count-Min (doi:10.1016/j.jalgor.2003.12.001), Flajolet, Fusy, Gandouet and Meunier (2007) for HyperLogLog, Broder (1997) for MinHash (doi:10.1109/SEQUEN.1997.666900), and Miller (1976) and Rabin (1980) for primality testing.

The theory is developed in the monograph Bernoulli Types; the project hub collects the papers, notes and this package.

Install

pip install bernoulli-types

The distribution is bernoulli-types because the name bernoulli is taken on PyPI; the import name is bernoulli. Python 3.11 or later; there are no runtime dependencies.

From source:

pip install "git+https://github.com/queelius/bernoulli.git#subdirectory=bernoulli-py"

# or, for development
git clone https://github.com/queelius/bernoulli.git
cd bernoulli/bernoulli-py
pip install -e ".[dev]"          # pytest, pytest-cov, hypothesis, ruff, mypy
pip install -e ".[notebooks]"    # Jupyter, numpy, scipy, matplotlib, sympy

Examples

The examples below are doctests: python -m doctest README.md runs them, and the test suite runs them too.

A noisy bit

BernoulliBool is an observed Boolean plus its channel rates. & and | treat both operands as independent observations of the same truth (two classifiers on one input, Miller-Rabin rounds on one n), where the rates are prior-free. and_with_prior covers two independent truths.

>>> from bernoulli import BernoulliBool, algebra, measures
>>> b1 = BernoulliBool(value=True, fpr=0.10, fnr=0.05)
>>> b2 = BernoulliBool(value=True, fpr=0.08, fnr=0.12)
>>> both = b1 & b2                      # shared truth: fpr1 * fpr2, 1 - (1-fnr1)(1-fnr2)
>>> round(both.fpr, 6), round(both.fnr, 6)
(0.008, 0.164)
>>> round(b1.and_with_prior(b2, prior=0.5).fpr, 4)   # independent truths, prior 1/2
0.0573
>>> algebra.miller_rabin_error(k=10)    # ten one-sided rounds: (1/4)**10
9.5367431640625e-07
>>> round(measures.ppv(fpr=0.05, fnr=0.10, prevalence=0.01), 4)   # base-rate effect
0.1538

Approximate sets

A Bloom filter and a perfect-hash filter both have fnr = 0. The perfect filter (a BBHash minimal perfect hash plus a 10-bit fingerprint per key) needs the key set up front and pays an additive overhead over the -log2(fpr) floor, where the Bloom filter pays a factor of about 1.44.

>>> from bernoulli import BloomFilter, PerfectFilter
>>> from bernoulli.sets.entropy_bound import entropy_floor_bits_per_key
>>> keys = [f"user-{i}".encode() for i in range(5000)]
>>> bloom = BloomFilter(capacity=len(keys), target_fpr=2**-10)
>>> for k in keys:
...     bloom.add(k)
>>> perfect = PerfectFilter(keys, target_fpr=2**-10)
>>> all(bloom.contains(k) and perfect.contains(k) for k in keys)   # no false negatives
True
>>> perfect.fpr()                       # 2**-10, by construction
0.0009765625
>>> others = [f"other-{i}".encode() for i in range(50_000)]
>>> sum(map(perfect.contains, others)) / len(others)   # measured on non-members
0.00094
>>> sum(map(bloom.contains, others)) / len(others)     # the Bloom formula assumes ideal hashes
0.00128
>>> entropy_floor_bits_per_key(2**-10), round(perfect.bits_per_key(), 2), round(bloom.bits_per_key(), 2)
(10.0, 13.35, 14.43)

Composition, and where a rate pair stops being enough

Set operations propagate rates, and the result depends on where the queries fall (book chapter 6). An intersection's false-positive rate is the product only for queries outside both sets.

>>> from bernoulli import BernoulliBool, Partition, SetRates, formula
>>> from bernoulli.sets.composition import intersection
>>> a = b = SetRates(fpr=0.01, fnr=0.0)
>>> round(intersection(a, b, Partition.sparse()).fpr, 6)   # queries outside A and B
0.0001
>>> in_a = Partition(both=0.0, a_only=1.0, b_only=0.0, neither=0.0)
>>> round(intersection(a, b, in_a).fpr, 6)                 # queries known to be in A
0.01

A pair of rates is exact for one operation. It does not compose exactly through a Boolean tree over different latent bits, because a gate's error depends on which latent word occurred. bernoulli.formula computes the exact error at any latent word and a worst-case pair that bounds every word:

>>> x = formula.variables(3)
>>> f = (x[0] & x[1]) | x[2]
>>> channels = [SetRates(fpr=0.02, fnr=0.10)] * 3
>>> bit = BernoulliBool(value=False, fpr=0.02, fnr=0.10)
>>> round(((bit & bit) | bit).fpr, 6)   # one pair reused at every gate
0.020392
>>> round(formula.error_probability(f, channels, (False, True, False)), 6)   # exact at 010
0.03764
>>> worst = formula.worst_case_rates(f, channels)
>>> round(worst.fpr, 6), round(worst.fnr, 6)
(0.03764, 0.1862)

A codec decides what non-members see

A retrieval structure stores key -> value in about r bits per key without storing keys, so a non-member still gets an answer. Through a codec, that answer follows the codec's codespace shares rather than the stored values: below, c and d are never stored but still come back for non-members.

>>> from collections import Counter
>>> from bernoulli import EncodedRetrieval, PrefixCodec
>>> codec = PrefixCodec.from_frequencies({"a": 4, "b": 2, "c": 1, "d": 1}, m_bits=4)
>>> sorted(codec.non_member_distribution().items())
[('a', 0.5), ('b', 0.25), ('c', 0.125), ('d', 0.125)]
>>> data = {f"key-{i}".encode(): "ab"[i % 2] for i in range(2000)}
>>> retrieval = EncodedRetrieval(data, codec)
>>> all(retrieval.query(k) == v for k, v in data.items())   # exact on the build set
True
>>> seen = Counter(retrieval.query(f"probe-{i}".encode()) for i in range(20_000))
>>> {v: round(seen[v] / 20_000, 2) for v in "abcd"}
{'a': 0.5, 'b': 0.25, 'c': 0.12, 'd': 0.13}

What is implemented

Everything below is pure Python with no runtime dependencies. The phases follow the monograph; PHASING.md has the roadmap.

Phase Module Contents
1 Atom bernoulli.atom BernoulliBool: noisy bit, ~ & |, prior-dependent AND / OR
bernoulli.algebra composition theorem 1 - prod(1 - eta_i), k-fold chains, Bloom sizing
bernoulli.measures PPV, NPV, TPR, TNR, Youden's J, F1, MCC, balanced accuracy
bernoulli.formula exact error of Boolean formulas over independent noisy bits: per latent word, worst case, prior-weighted marginal
2 Sets bernoulli.sets MembershipOracle protocol, BloomFilter
bernoulli.sets.composition SetRates, Partition; union, intersection, complement, difference
bernoulli.sets.entropy_bound -log2(fpr) floor, Bloom 1.44x overhead, perfect-filter space model
bernoulli.sets.cascade, .correlated_bloom research helpers: Bloom cascades, filters with shared hash salts
3 Maps bernoulli.maps PerfectHashFunction, ApproximateMap protocols; MinimalPerfectHash (BBHash); PerfectFilter
bernoulli.maps.codec DenseCodec, PaddedCodec, PrefixCodec (Huffman), shannon_entropy
bernoulli.maps.retrieval GF2Retrieval (peeling), EncodedRetrieval (retrieval plus codec)
4 Relations bernoulli.relations BernoulliRelation: exact tuples carrying membership rates through select, project, join
5 Sketches bernoulli.sketches CountMinSketch, HyperLogLog, MinHash
bernoulli.algorithms Miller-Rabin as a one-sided Bernoulli bit; a configurable noisy classifier
Tools bernoulli.harness, bernoulli.bench measured rates with Wilson intervals; best-of-N timing

Not implemented yet:

  • C acceleration. There is no C code. c/ holds a design note for a planned optional cffi core with pure-Python fallbacks, and no [c] extra is offered until it exists.
  • The banded ribbon solver (the peeling construction is the reference), Las Vegas and Monte Carlo adapters, and an approximate relation store.

The repository also holds 16 validation experiments (experiments/, summarized in SYNTHESIS.md) and 15 notebooks that accompany the monograph (notebooks/). Neither ships in the wheel.

Limits

  • No security claims. The hashes are FNV-1a and splitmix64, chosen for speed and reproducibility. They are not cryptographic, and nothing in this package supports a claim about adversaries, privacy or secrecy.
  • Rate pairs are summaries. A single (fpr, fnr) is exact for one gate or one set operation, and & / | are exact for read-once AND / OR trees of independent observations of one bit. Through a general Boolean formula over different latent bits, reusing pairs can understate the error (0.020392 against 0.03764 in the example above). Use bernoulli.formula for exact per-word values or a sound worst-case pair.
  • Independence is assumed, not checked. The composition rules assume independent error events across operands. Filters that share hash randomness, or an operand used twice, violate it; experiment 01 measures a 19x error when two Bloom filters share salt and keys.
  • Composition works on rates, not on live structures. SetRates and Partition propagate numbers. There is no bloom_a & bloom_b that returns a queryable filter.
  • BernoulliRelation is not an approximate store. It keeps exact tuples; its rates describe what an approximate representation would do.
  • No save or load. Nothing is serializable yet.
  • Static structures. MinimalPerfectHash, PerfectFilter and GF2Retrieval are built once from a known key set. Keys are bytes.
  • Speed. Pure Python suits experiments, teaching and moderate key counts, not production throughput.
  • Reported rates are models. BloomFilter.fpr() is the asymptotic formula at the current load and PerfectFilter.fpr() is 2**-r; measured rates on real keys differ (see the Bloom example). bernoulli.harness measures them.

Development

From bernoulli-py/:

uv venv && VIRTUAL_ENV="$PWD/.venv" uv pip install -e ".[dev]"
.venv/bin/python -m pytest --cov=bernoulli --cov-branch --cov-report=term-missing
.venv/bin/ruff check src tests
.venv/bin/mypy

The changelog is CHANGELOG.md.

Citing

See CITATION.cff. For the theory, cite the monograph.

License

MIT; see LICENSE.

Author

Alexander Towell (lex@metafunctor.com, ORCID 0000-0001-6443-9897), Southern Illinois University Edwardsville.

Metadata

Release files for bernoulli-types 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bernoulli-types 0.3.0
File Size Uploaded Provenance
bernoulli_types-0.3.0.tar.gz 99.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for bernoulli-types 0.3.0
File Interpreter ABI Platform
bernoulli_types-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 178.0 kB

Release files / bernoulli_types-0.3.0.tar.gz

Download URL bernoulli_types-0.3.0.tar.gz
Size 99.2 kB
Tags Source
SHA-256 checksum
How to use checksums
062c7d72f9f5dd0a2677e2c9d2e5a768d2c199c331d8fa6f35f73289c2aed889
BLAKE2b-256 checksum
How to use checksums
de6863aec072e3356e95baeb42ce0b0c07b07ee9ed6094520b6ae99d5c11b02d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.15

Release files / bernoulli_types-0.3.0-py3-none-any.whl

Download URL bernoulli_types-0.3.0-py3-none-any.whl
Size 78.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
78f29af2eda4eebae8db301f9c7a0e5231169115b056b965916409032d66c825
BLAKE2b-256 checksum
How to use checksums
c132e0f6040a170536e9e9ca47cffd03bf8182bbc718de9ac2babd337683d3cd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.15

Security for bernoulli-types 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page