DigitBinIndex
A DigitBinIndex is a tree-based data structure that organizes a large collection of weighted items to enable highly efficient weighted random selection and removal. It is a specialized tool, purpose-built for scenarios with millions of items where probabilities are approximate and high performance is critical.
This library provides state-of-the-art, high-performance solutions for both major types of noncentral hypergeometric distributions:
- Sequential Sampling (Wallenius'): Modeled by
select_and_remove. - Simultaneous Sampling (Fisher's): Modeled by
select_many_and_remove.
The Core Problem
In many simulations, forecasts, or statistical models, one needs to manage a large, dynamic set of probabilities. A common task is to randomly select items based on their weight, remove them, and repeat. Doing this efficiently with millions of items is a non-trivial performance challenge, especially when modeling complex behaviors like Wallenius' or Fisher's distributions, which are common in agent-based simulations like mortality models.
How It Works
DigitBinIndex is a radix tree where the path is determined by the decimal digits of the probabilities. This structure allows it to group items into "bins" based on a configurable level of precision.
-
Digit-based Tree Structure: The index builds a tree where each level corresponds to a decimal place. For a probability like
0.543, an item would be placed by traversing the path:root -> child[5] -> child[4] -> child[3]. -
Roaring Bitmap Bins: The node at the end of a path acts as a "bin." Instead of a simple list, it holds a Roaring Bitmap, a highly optimized data structure for storing and performing set operations on integers. This is the key to the library's high performance for simultaneous (Fisher's) draws.
-
Accumulated Value Index: Each node in the tree stores the
accumulated_value(the sum of all probabilities beneath it). This allows for extremely fastO(P)weighted random selection, wherePis the configured precision.
Features
- State-of-the-Art Performance: Outperforms standard, general-purpose data structures for both sequential and simultaneous weighted sampling.
- Dual-Model Support: Provides optimized methods for both Wallenius' (
select_and_remove) and Fisher's (select_many_and_remove) distributions. - Effectively O(1) Complexity: Core operations have a time complexity of
O(P), wherePis the configured precision. This is effectively constant time, independent of the number of items. - Memory Efficient: The combination of a sparse tree and Roaring Bitmaps makes it highly memory-efficient for most datasets.
Performance
DigitBinIndex makes a deliberate engineering trade-off: it sacrifices a small, controllable amount of precision by binning probabilities to gain significant improvements in speed.
The standard alternative is a Fenwick Tree, which is perfectly accurate but has a slower O(log N) complexity. The benchmarks below compare DigitBinIndex against a highly optimized Fenwick Tree implementation.
Wallenius' Draw (Sequential Selections)
This benchmark measures the total time to perform a loop of 1,000 select_and_remove operations. The results show DigitBinIndex's superior O(P) complexity provides a massive and growing advantage as the dataset size increases.
| Number of Items (N) | DigitBinIndex Loop Time |
FenwickTree Loop Time |
Speedup Factor |
|---|---|---|---|
| 100,000 | ~0.46 ms | ~1.77 ms | ~3.9x faster |
| 1,000,000 | ~0.52 ms | ~13.58 ms | ~26.1x faster |
Fisher's Draw (Simultaneous Selections)
This benchmark measures the time to select a single batch of unique items (1% of the total population). After algorithmic improvements, DigitBinIndex now uses a batched rejection sampling approach that is significantly more efficient than its previous method and faster than the Fenwick Tree's equivalent.
| Scenario (N items, draw k) | DigitBinIndex Time |
FenwickTree Time |
Speedup Factor |
|---|---|---|---|
| N=100k, k=1k | ~0.47 ms | ~1.87 ms | ~4.0x faster |
| N=1M, k=10k | ~5.48 ms | ~20.16 ms | ~3.7x faster |
As the results show, DigitBinIndex outperforms the Fenwick Tree in both sequential and simultaneous batched selection scenarios, making it a highly effective tool for large-scale weighted random sampling simulations.
When to Choose DigitBinIndex
This structure is the preferred choice when your scenario matches these conditions:
- You need high-performance Wallenius' or Fisher's sampling.
- Your dataset is large (
N> 100,000). - Your probabilities are approximate. If your weights come from empirical data, simulations, or ML models, the precision beyond a few decimal places is often meaningless.
- Performance is more critical than perfect precision.
You should consider a more general-purpose data structure (like a Fenwick Tree) only if you require perfect, lossless precision and your data is "digitally incompressible" (e.g., all items differ only at a very high decimal place).
Choosing a Precision
The precision parameter controls the depth of the radix tree and represents the core trade-off of the library: Accuracy vs. Performance & Memory. Choosing the right value is key to getting the most out of DigitBinIndex.
The Rule of Thumb
For most applications, a precision of 3 or 4 is an excellent starting point. This provides a great balance, capturing the vast majority of the weight distribution while remaining extremely fast.
The Mathematical Intuition
The impact of each decimal place on an item's probability diminishes exponentially. Consider a weight of 0.12345:
- The 1st digit (
1) contributes0.1to the value. - The 2nd digit (
2) contributes0.02. - The 3rd digit (
3) contributes0.003. - The 4th digit (
4) contributes only0.0004.
By truncating at 3 digits, the maximum error for any single item is less than 0.001. When sampling from a large population, these small, random errors tend to average out, having a negligible effect on the final distribution of selections. The first few digits capture almost all of the meaningful relative differences between item weights, which is what drives a weighted random draw.
Guidance
Here is a summary to help guide your choice:
| Precision | Typical Use Case | Trade-offs |
|---|---|---|
| 1-2 | Maximum performance, minimal memory usage. | Best for coarse-grained weights (e.g., 0.1, 0.5, 0.9). Loses significant accuracy with finely-grained data. |
| 3-4 | Recommended Default. The optimal balance for most scenarios. | Captures sufficient detail for typical floating-point data from simulations or models, with negligible performance cost. |
| 5+ | High-fidelity scenarios where weights are very close. | Use if you must distinguish between weights like 0.12345 and 0.12346. This increases memory usage and is slightly slower, but still O(P). |
Usage
First, add digit-bin-index to your project's dependencies in Cargo.toml. You will also need rust_decimal because it is used in the public API for defining item weights.
[]
= "0.2.2" # Use the latest version from crates.io
= "1.37" # The decimal type used in the API
= "1.37" # Recommended for easily creating decimals
Then, you can use DigitBinIndex in your project to perform both sequential (Wallenius') and simultaneous (Fisher's) draws.
use DigitBinIndex;
use dec; // A convenient macro, dec!
License
This project is licensed under the MIT License.