t-digest
A Rust implementation of the t-digest data structure for accurate online accumulation of rank-based statistics such as quantiles and trimmed means, using a variant of 1-dimensional k-means clustering.
This implementation follows Facebook's folly TDigest.
Features
- Accurate quantile estimation with bounded error, especially at the tails (p99, p999)
- Mergeable -- combine digests computed on different machines or threads
- Compact -- fixed memory footprint regardless of input size
- No dependencies by default (optional
serdesupport behind a feature flag) no_std+allocsupport, including serde without itsstdfeature
Installation
Add this to your Cargo.toml:
[]
= "1.0"
Optional features
| Feature | Description |
|---|---|
std |
Enabled by default; disable it for no_std builds |
serde |
Enables Serialize/Deserialize for TDigest and Centroid |
use_serde |
Deprecated compatibility alias for serde |
[]
= { = "1.0", = ["serde"] }
Quick start
use TDigest;
// Create a digest with a compression factor of 100
let t = new_with_size;
// Feed it one million values
let values: = .map.collect;
let t = t.merge_sorted;
// Estimate quantiles
let p99 = t.estimate_quantile.unwrap;
let expected = 990_000.0;
assert!;
Usage
Creating a digest
use TDigest;
// With explicit compression factor (controls accuracy vs. memory)
let t = new_with_size;
// With default settings (max_size = 100)
let t = default;
Adding data
use TDigest;
let t = new_with_size;
// Pre-sorted data (fastest)
let sorted = vec!;
let t = t.merge_sorted;
// Unsorted data (sorts internally)
let unsorted = vec!;
let t = t.merge_unsorted;
For streaming ingestion, use the buffered mutable API and flush before a centroid-based query:
use TDigest;
let mut t = new_with_size;
for value in
t.flush;
assert_eq!;
Querying
use TDigest;
let t = new_with_size;
let t = t.merge_sorted;
// Returns None for empty digests, Some(value) otherwise
let median = t.estimate_quantile; // ~3.0
let min = t.min; // Some(1.0)
let max = t.max; // Some(5.0)
let mean = t.mean; // Some(3.0)
let count = t.count; // 5.0
let sum = t.sum; // 15.0
Merging digests (distributed / parallel use)
use TDigest;
// Build digests independently (e.g. on different threads)
let mut digests = Vecnew;
for chunk in data.chunks
// Merge into a single digest
let combined = merge_digests;
let p99 = combined.estimate_quantile;
Serialization (serde)
Enable the serde feature, then use any serde-compatible format. The deprecated
use_serde alias remains available for existing users.
use TDigest;
let t = new_with_size;
let t = t.merge_sorted;
let json = to_string.unwrap;
let restored: TDigest = from_str.unwrap;
Compression factor
The max_size parameter controls the trade-off between accuracy and memory:
max_size |
Memory (approx.) | Accuracy |
|---|---|---|
| 50 | ~1.2 KB | Good for rough estimates |
| 100 | ~2.4 KB | Good default |
| 200 | ~4.8 KB | High accuracy |
| 500 | ~12 KB | Very high accuracy |
Larger values produce more centroids, giving better accuracy at the cost of more memory and slightly slower merges. A value of 100 is sufficient for most use cases.
Minimum Supported Rust Version (MSRV)
Rust 1.62 -- verified in CI.
no_std
Disable default features to use the crate with alloc but without std:
[]
= { = "1.0", = false }
Serde also works in this configuration by adding features = ["serde"].
Accuracy
Run cargo run --release --example accuracy to compare estimated and exact
quantiles across deterministic uniform, normal, lognormal, exponential, bimodal,
and adversarial distributions. See architecture and internals
for a summary of the measured error profile.
Benchmarks
Run cargo bench to measure sorted and unsorted batch ingestion, incremental
ingestion, digest merging, quantile queries, and compression-size sensitivity.
The benchmark inputs use fixed random seeds so results can be compared across
changes. Run cargo run --release --example accuracy for the accuracy harness.
Documentation
License
Apache-2.0 -- see LICENSE for details.