Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std.sketch

Bounded summaries for streams too large to retain. These pure-Plenty algorithms own their memory and random state. They do not wrap Apache DataSketches or share its serialized formats. Also see xor filters and cuckoo filters.

from std.sketch import SketchError, hyperloglog, hash_u64, quantiles

def main() -> Result[(), SketchError | AllocError | IoError | ZeroDivisionError]:
    mut users = hyperloglog()?
    mut latency = quantiles()?
    for i in range(10000):
        user = u64(i % 2500)
        users.add(hash_u64(user))
        latency.add(f64(i % 100))?
    print(f"about {users.estimate():.0f} distinct users"?)?
    print("median", latency.quantile(0.5)?, "max", latency.quantile(1.0)?)?
    Ok(())
about 2511 distinct users
median 47.0 max 99.0

Distinct identities

hyperloglog(precision=12, seed=0) creates a dense HyperLogLog. Precision is 4–18; storage is exactly 2**precision packed one-byte registers plus Bytes metadata. add(key: u64) and merge(other) allocate nothing. Duplicate keys are harmless. Merge requires equal precision and seed and leaves the receiver unchanged on SketchError.Incompatible. Estimate and merge cost O(registers).

estimate() uses the original HyperLogLog harmonic estimate with linear counting for small cardinalities. relative_standard_error() returns 1.04/sqrt(m): an asymptotic standard error, not a guaranteed error bound. There is no HLL++ bias table or near-64-bit-universe correction. Use this for ordinary cardinality estimation, not exact counts or estimates approaching the hash-space size. See the HyperLogLog paper.

Keys are numeric identities. hash_u64(key, seed=0) is a stable SplitMix64 mixer, not a cryptographic hash. The algorithms are designed for ordinary data, not adversarially chosen keys. Applications mapping text or compound values to u64 must choose and persist their own stable hash convention; collisions in that mapping cannot be undone by a sketch.

Quantiles

quantiles(capacity: Positive[i64] = 1024, seed=0) creates Quantiles, a uniform sample without replacement using reservoir Algorithm R. Capacity is up to 1,048,576 finite f64 values; a larger capacity traps. add(value) rejects NaN/infinities; count() is the number of accepted values and retained() is at most capacity. Construction reserves the sample buffer; updates allocate nothing, maintain it in sorted order, and cost O(capacity) when replacing a sample. Queries cost O(1).

quantile(fraction) accepts a finite number in [0,1]. Empty streams return SketchError.Empty. Endpoints return the exact minimum/maximum. Other fractions use the nearest-rank order statistic of the retained sample (no interpolation). Results are exact before sampling starts. Afterwards they are probabilistic; there is no deterministic rank-error guarantee or tail-relative guarantee. Increase capacity to improve accuracy. This first API has no quantile merge. It is a simple reservoir, not KLL, REQ, or a t-digest.

The sketch owns a std.random.Rng. The seed, accepted input order, and capacity determine its sample. Serializing and restoring preserves continued sampling. The random generator’s existing version-stability contract is unchanged.

Frequent identities

frequent_items(capacity: Positive[i64] = 128) creates a FrequentItems Misra–Gries summary with up to 65,536 counters; a larger capacity traps. It accepts unit-weight u64 identities; there is no weighted update or merge in this API. Construction allocates two fixed arrays. add(key) and frequency(key) allocate nothing and cost O(capacity).

frequency(key) returns Frequency(key, lower, upper) with deterministic bounds on the true count. error_bound() is the number of full counter decrements, at most count() // (capacity + 1). A missing candidate has lower bound zero and upper bound error_bound(). candidates() allocates a list of retained nonzero counters in storage order, not frequency order. Every key whose true frequency exceeds error_bound() appears in that list. A second pass over the original data can compute exact counts for these candidates.

Errors and persistence

InvalidArgument covers invalid configuration or nonfinite input. Count overflow returns Limit without accepting the update: these numerical counters stay exact, never wrap or saturate, and a rejected update leaves samples, extrema, RNG state and frequency bounds unchanged. Nonfinite quantile input is still InvalidArgument, even when the count is exhausted. Constructors, readers and updates that can also fail for another reason report allocation failure as SketchError.Allocation(cause); candidates() and to_bytes() fail only to allocate and return AllocError. All allocation failures remain recoverable.

to_bytes() writes a versioned Plenty format: PSH1, PSQ1, or PSF1. Integers use little-endian u64 words. Quantile values use 32-byte, space-padded, shortest round-trip scientific ASCII slots; this preserves finite f64 values, including signed zero, without host-endian assumptions. These are checkpoint formats, not interchange formats with third-party sketch libraries.

hyperloglog_from_bytes(data, max_precision=18), quantiles_from_bytes(data, max_capacity: Positive[i64] = 1048576), and frequent_items_from_bytes(data, max_capacity: Positive[i64] = 65536) reject unknown versions, bad lengths, invalid state and trailing bytes. Pass a smaller caller limit when reading untrusted checkpoints. All three readers take a &[u8] view. Validated precision/capacity bounds keep checkpoint sizes and offsets representable; frequent-item accounting is validated against the serialized total before adding each counter. Formats are version-tagged: future incompatible formats need a new version and must not silently reinterpret these bytes. They contain no checksum or authentication; structurally valid corruption can change estimates.

The crate audit for issues #496–#498 found infallible allocations in datasketches 0.5.0 (hll/array8.rs, req/compactor.rs), xorf 0.13.0 (prelude/mod.rs), and scalable_cuckoo_filter 0.5.1 (bits.rs). Small direct implementations keep Plenty’s recoverable allocation policy without a native dependency or ABI.

Acknowledgements

  • Uses the HyperLogLog algorithm by Philippe Flajolet, Éric Fusy, Olivier Gandouet and Frédéric Meunier (2007).
  • Uses reservoir Algorithm R as described by Jeffrey Vitter (1985).