cb_digest/
lib.rs

1//! T-Digest algorithm in rust
2//!
3//! ## Installation
4//!
5//! Add this to your `Cargo.toml`:
6//!
7//! ```toml
8//! [dependencies]
9//! cb-digest = "0.2"
10//! ```
11//!
12//! then you are good to go. If you are using Rust 2015 you have to ``extern crate cb-digest`` to your crate root as well.
13//!
14//! ## Example
15//!
16//! ```rust
17//! use cb_digest::TDigest;
18//!
19//! let t = TDigest::new_with_size(100);
20//! let values: Vec<f64> = (1..=1_000_000).map(f64::from).collect();
21//!
22//! let t = t.merge_sorted(values);
23//!
24//! let ans = t.estimate_quantile(0.99);
25//! let expected: f64 = 990_000.0;
26//!
27//! let percentage: f64 = (expected - ans).abs() / expected;
28//! assert!(percentage < 0.01);
29//! ```
30
31use ordered_float::OrderedFloat;
32use std::cmp::Ordering;
33
34#[cfg(feature = "use_serde")]
35use serde::{Deserialize, Serialize};
36
37#[cfg(feature = "use_rkyv")]
38use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
39
40/// Centroid implementation to the cluster mentioned in the paper.
41#[derive(Debug, PartialEq, Eq, Clone)]
42#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
43#[cfg_attr(feature = "use_rkyv", derive(Archive, RkyvDeserialize, RkyvSerialize))]
44pub struct Centroid {
45    mean: OrderedFloat<f64>,
46    weight: OrderedFloat<f64>,
47}
48
49impl PartialOrd for Centroid {
50    fn partial_cmp(&self, other: &Centroid) -> Option<Ordering> {
51        Some(self.cmp(other))
52    }
53}
54
55impl Ord for Centroid {
56    fn cmp(&self, other: &Centroid) -> Ordering {
57        self.mean.cmp(&other.mean)
58    }
59}
60
61impl Centroid {
62    pub fn new(mean: f64, weight: f64) -> Self {
63        Centroid {
64            mean: OrderedFloat::from(mean),
65            weight: OrderedFloat::from(weight),
66        }
67    }
68
69    #[inline]
70    pub fn mean(&self) -> f64 {
71        self.mean.into_inner()
72    }
73
74    #[inline]
75    pub fn weight(&self) -> f64 {
76        self.weight.into_inner()
77    }
78
79    pub fn add(&mut self, sum: f64, weight: f64) -> f64 {
80        let weight_: f64 = self.weight.into_inner();
81        let mean_: f64 = self.mean.into_inner();
82
83        let new_sum: f64 = sum + weight_ * mean_;
84        let new_weight: f64 = weight_ + weight;
85        self.weight = OrderedFloat::from(new_weight);
86        self.mean = OrderedFloat::from(new_sum / new_weight);
87        new_sum
88    }
89}
90
91impl Default for Centroid {
92    fn default() -> Self {
93        Centroid {
94            mean: OrderedFloat::from(0.0),
95            weight: OrderedFloat::from(1.0),
96        }
97    }
98}
99
100/// T-Digest to be operated on.
101#[derive(Debug, PartialEq, Eq, Clone)]
102#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
103#[cfg_attr(feature = "use_rkyv", derive(Archive, RkyvDeserialize, RkyvSerialize))]
104pub struct TDigest {
105    centroids: Vec<Centroid>,
106    max_size: usize,
107    sum: OrderedFloat<f64>,
108    count: OrderedFloat<f64>,
109    max: OrderedFloat<f64>,
110    min: OrderedFloat<f64>,
111}
112
113impl TDigest {
114    pub fn new_with_size(max_size: usize) -> Self {
115        TDigest {
116            centroids: Vec::new(),
117            max_size,
118            sum: OrderedFloat::from(0.0),
119            count: OrderedFloat::from(0.0),
120            max: OrderedFloat::from(std::f64::NAN),
121            min: OrderedFloat::from(std::f64::NAN),
122        }
123    }
124
125    pub fn new(centroids: Vec<Centroid>, sum: f64, count: f64, max: f64, min: f64, max_size: usize) -> Self {
126        if centroids.len() <= max_size {
127            TDigest {
128                centroids,
129                max_size,
130                sum: OrderedFloat::from(sum),
131                count: OrderedFloat::from(count),
132                max: OrderedFloat::from(max),
133                min: OrderedFloat::from(min),
134            }
135        } else {
136            let sz = centroids.len();
137            let digests: Vec<TDigest> = vec![
138                TDigest::new_with_size(100),
139                TDigest::new(centroids, sum, count, max, min, sz),
140            ];
141
142            Self::merge_digests(digests)
143        }
144    }
145
146    #[inline]
147    pub fn mean(&self) -> f64 {
148        let count_: f64 = self.count.into_inner();
149        let sum_: f64 = self.sum.into_inner();
150
151        if count_ > 0.0 {
152            sum_ / count_
153        } else {
154            0.0
155        }
156    }
157
158    #[inline]
159    pub fn sum(&self) -> f64 {
160        self.sum.into_inner()
161    }
162
163    #[inline]
164    pub fn count(&self) -> f64 {
165        self.count.into_inner()
166    }
167
168    #[inline]
169    pub fn max(&self) -> f64 {
170        self.max.into_inner()
171    }
172
173    #[inline]
174    pub fn min(&self) -> f64 {
175        self.min.into_inner()
176    }
177
178    #[inline]
179    pub fn is_empty(&self) -> bool {
180        self.centroids.is_empty()
181    }
182
183    #[inline]
184    pub fn max_size(&self) -> usize {
185        self.max_size
186    }
187
188    /// Add a single value to the digest
189    pub fn add(&mut self, value: f64) {
190        // Update basic statistics
191        let prev_count = self.count.into_inner();
192        self.count = OrderedFloat::from(prev_count + 1.0);
193        self.sum = OrderedFloat::from(self.sum.into_inner() + value);
194        
195        // Update min/max
196        if prev_count == 0.0 {
197            self.min = OrderedFloat::from(value);
198            self.max = OrderedFloat::from(value);
199        } else {
200            if value < self.min.into_inner() {
201                self.min = OrderedFloat::from(value);
202            }
203            if value > self.max.into_inner() {
204                self.max = OrderedFloat::from(value);
205            }
206        }
207        
208        // If this is the first value, just create a centroid
209        if self.centroids.is_empty() {
210            self.centroids.push(Centroid::new(value, 1.0));
211            return;
212        }
213        
214        // Find where this value would fit in the sorted centroids
215        let pos = self.centroids
216            .binary_search_by(|c| c.mean.partial_cmp(&OrderedFloat::from(value)).unwrap())
217            .unwrap_or_else(|i| i);
218        
219        // Try to merge with nearby centroids if within the compression bounds
220        let mut merged = false;
221        let total_count = self.count.into_inner();
222        
223        // Check if we can merge with the centroid at or before this position
224        if pos > 0 {
225            let idx = pos - 1;
226            if self.can_merge_at(idx, 1.0, total_count) {
227                let weight = self.centroids[idx].weight();
228                let mean = self.centroids[idx].mean();
229                let new_mean = (mean * weight + value) / (weight + 1.0);
230                self.centroids[idx] = Centroid::new(new_mean, weight + 1.0);
231                merged = true;
232            }
233        }
234        
235        // If not merged yet, try the centroid at this position
236        if !merged && pos < self.centroids.len() {
237            if self.can_merge_at(pos, 1.0, total_count) {
238                let weight = self.centroids[pos].weight();
239                let mean = self.centroids[pos].mean();
240                let new_mean = (mean * weight + value) / (weight + 1.0);
241                self.centroids[pos] = Centroid::new(new_mean, weight + 1.0);
242                merged = true;
243            }
244        }
245        
246        // If we couldn't merge, insert as a new centroid
247        if !merged {
248            self.centroids.insert(pos, Centroid::new(value, 1.0));
249            
250            // Compress if we've exceeded max_size
251            if self.centroids.len() > self.max_size {
252                self.compress();
253            }
254        }
255    }
256    
257    /// Check if we can merge a new weight at the given position
258    fn can_merge_at(&self, idx: usize, new_weight: f64, total_count: f64) -> bool {
259        // Calculate the k value for this centroid
260        let mut k = 0.0;
261        for i in 0..idx {
262            k += self.centroids[i].weight();
263        }
264        k += self.centroids[idx].weight() / 2.0;
265        
266        // Calculate the quantile limits
267        let q_current = Self::k_to_q(k, self.max_size as f64);
268        let q_next = Self::k_to_q(k + self.centroids[idx].weight() / 2.0 + new_weight, self.max_size as f64);
269        
270        // Check if adding this weight would exceed the compression bound
271        let current_bound = q_current * total_count;
272        let next_bound = q_next * total_count;
273        let new_total = self.centroids[idx].weight() + new_weight;
274        
275        new_total <= (next_bound - current_bound)
276    }
277    
278    /// Compress the digest when it exceeds max_size
279    fn compress(&mut self) {
280        if self.centroids.len() <= self.max_size {
281            return;
282        }
283        
284        let mut compressed: Vec<Centroid> = Vec::with_capacity(self.max_size);
285        let total_count = self.count.into_inner();
286        
287        let mut k_limit = 1.0;
288        let mut q_limit_times_count = Self::k_to_q(k_limit, self.max_size as f64) * total_count;
289        
290        let mut iter = self.centroids.iter();
291        let mut curr = iter.next().unwrap().clone();
292        let mut weight_so_far = curr.weight();
293        
294        for centroid in iter {
295            weight_so_far += centroid.weight();
296            
297            if weight_so_far <= q_limit_times_count {
298                // Merge into current
299                let combined_weight = curr.weight() + centroid.weight();
300                let combined_mean = (curr.mean() * curr.weight() + centroid.mean() * centroid.weight()) / combined_weight;
301                curr = Centroid::new(combined_mean, combined_weight);
302            } else {
303                // Save current and start new
304                compressed.push(curr);
305                curr = centroid.clone();
306                k_limit += 1.0;
307                q_limit_times_count = Self::k_to_q(k_limit, self.max_size as f64) * total_count;
308            }
309        }
310        
311        compressed.push(curr);
312        self.centroids = compressed;
313    }
314    
315    /// Batch add multiple values efficiently (more efficient than multiple add() calls)
316    pub fn add_batch(&mut self, values: &[f64]) {
317        if values.len() > 20 {
318            // For larger batches, use the merge approach as it's more efficient
319            let sorted: Vec<f64> = {
320                let mut v = values.to_vec();
321                v.sort_by(|a, b| a.partial_cmp(b).unwrap());
322                v
323            };
324            *self = self.merge_sorted(sorted);
325        } else {
326            // For small batches, add individually
327            for &value in values {
328                self.add(value);
329            }
330        }
331    }
332}
333
334impl Default for TDigest {
335    fn default() -> Self {
336        TDigest {
337            centroids: Vec::new(),
338            max_size: 100,
339            sum: OrderedFloat::from(0.0),
340            count: OrderedFloat::from(0.0),
341            max: OrderedFloat::from(std::f64::NAN),
342            min: OrderedFloat::from(std::f64::NAN),
343        }
344    }
345}
346
347impl TDigest {
348    fn k_to_q(k: f64, d: f64) -> f64 {
349        let k_div_d = k / d;
350        if k_div_d >= 0.5 {
351            let base = 1.0 - k_div_d;
352            1.0 - 2.0 * base * base
353        } else {
354            2.0 * k_div_d * k_div_d
355        }
356    }
357
358    fn clamp(v: f64, lo: f64, hi: f64) -> f64 {
359        if v > hi {
360            hi
361        } else if v < lo {
362            lo
363        } else {
364            v
365        }
366    }
367
368    pub fn merge_unsorted(&self, unsorted_values: Vec<f64>) -> TDigest {
369        let mut sorted_values: Vec<OrderedFloat<f64>> = unsorted_values.into_iter().map(OrderedFloat::from).collect();
370        sorted_values.sort();
371        let sorted_values = sorted_values.into_iter().map(|f| f.into_inner()).collect();
372
373        self.merge_sorted(sorted_values)
374    }
375
376    pub fn merge_sorted(&self, sorted_values: Vec<f64>) -> TDigest {
377        if sorted_values.is_empty() {
378            return self.clone();
379        }
380
381        let mut result = TDigest::new_with_size(self.max_size());
382        result.count = OrderedFloat::from(self.count() + (sorted_values.len() as f64));
383
384        let maybe_min = OrderedFloat::from(*sorted_values.first().unwrap());
385        let maybe_max = OrderedFloat::from(*sorted_values.last().unwrap());
386
387        if self.count() > 0.0 {
388            result.min = std::cmp::min(self.min, maybe_min);
389            result.max = std::cmp::max(self.max, maybe_max);
390        } else {
391            result.min = maybe_min;
392            result.max = maybe_max;
393        }
394
395        let mut compressed: Vec<Centroid> = Vec::with_capacity(self.max_size);
396
397        let mut k_limit: f64 = 1.0;
398        let mut q_limit_times_count: f64 = Self::k_to_q(k_limit, self.max_size as f64) * result.count.into_inner();
399        k_limit += 1.0;
400
401        let mut iter_centroids = self.centroids.iter().peekable();
402        let mut iter_sorted_values = sorted_values.iter().peekable();
403
404        let mut curr: Centroid = if let Some(c) = iter_centroids.peek() {
405            let curr = **iter_sorted_values.peek().unwrap();
406            if c.mean() < curr {
407                iter_centroids.next().unwrap().clone()
408            } else {
409                Centroid::new(*iter_sorted_values.next().unwrap(), 1.0)
410            }
411        } else {
412            Centroid::new(*iter_sorted_values.next().unwrap(), 1.0)
413        };
414
415        let mut weight_so_far: f64 = curr.weight();
416
417        let mut sums_to_merge: f64 = 0.0;
418        let mut weights_to_merge: f64 = 0.0;
419
420        while iter_centroids.peek().is_some() || iter_sorted_values.peek().is_some() {
421            let next: Centroid = if let Some(c) = iter_centroids.peek() {
422                if iter_sorted_values.peek().is_none() || c.mean() < **iter_sorted_values.peek().unwrap() {
423                    iter_centroids.next().unwrap().clone()
424                } else {
425                    Centroid::new(*iter_sorted_values.next().unwrap(), 1.0)
426                }
427            } else {
428                Centroid::new(*iter_sorted_values.next().unwrap(), 1.0)
429            };
430
431            let next_sum: f64 = next.mean() * next.weight();
432            weight_so_far += next.weight();
433
434            if weight_so_far <= q_limit_times_count {
435                sums_to_merge += next_sum;
436                weights_to_merge += next.weight();
437            } else {
438                result.sum = OrderedFloat::from(result.sum.into_inner() + curr.add(sums_to_merge, weights_to_merge));
439                sums_to_merge = 0.0;
440                weights_to_merge = 0.0;
441
442                compressed.push(curr.clone());
443                q_limit_times_count = Self::k_to_q(k_limit, self.max_size as f64) * result.count();
444                k_limit += 1.0;
445                curr = next;
446            }
447        }
448
449        result.sum = OrderedFloat::from(result.sum.into_inner() + curr.add(sums_to_merge, weights_to_merge));
450        compressed.push(curr);
451        compressed.shrink_to_fit();
452        compressed.sort();
453
454        result.centroids = compressed;
455        result
456    }
457
458    fn external_merge(centroids: &mut Vec<Centroid>, first: usize, middle: usize, last: usize) {
459        let mut result: Vec<Centroid> = Vec::with_capacity(centroids.len());
460
461        let mut i = first;
462        let mut j = middle;
463
464        while i < middle && j < last {
465            match centroids[i].cmp(&centroids[j]) {
466                Ordering::Less => {
467                    result.push(centroids[i].clone());
468                    i += 1;
469                }
470                Ordering::Greater => {
471                    result.push(centroids[j].clone());
472                    j += 1;
473                }
474                Ordering::Equal => {
475                    result.push(centroids[i].clone());
476                    i += 1;
477                }
478            }
479        }
480
481        while i < middle {
482            result.push(centroids[i].clone());
483            i += 1;
484        }
485
486        while j < last {
487            result.push(centroids[j].clone());
488            j += 1;
489        }
490
491        i = first;
492        for centroid in result.into_iter() {
493            centroids[i] = centroid;
494            i += 1;
495        }
496    }
497
498    // Merge multiple T-Digests
499    pub fn merge_digests(digests: Vec<TDigest>) -> TDigest {
500        let n_centroids: usize = digests.iter().map(|d| d.centroids.len()).sum();
501        if n_centroids == 0 {
502            return TDigest::default();
503        }
504
505        let max_size = digests.first().unwrap().max_size;
506        let mut centroids: Vec<Centroid> = Vec::with_capacity(n_centroids);
507        let mut starts: Vec<usize> = Vec::with_capacity(digests.len());
508
509        let mut count: f64 = 0.0;
510        let mut min = OrderedFloat::from(std::f64::INFINITY);
511        let mut max = OrderedFloat::from(std::f64::NEG_INFINITY);
512
513        let mut start: usize = 0;
514        for digest in digests.into_iter() {
515            starts.push(start);
516
517            let curr_count: f64 = digest.count();
518            if curr_count > 0.0 {
519                min = std::cmp::min(min, digest.min);
520                max = std::cmp::max(max, digest.max);
521                count += curr_count;
522                for centroid in digest.centroids {
523                    centroids.push(centroid);
524                    start += 1;
525                }
526            }
527        }
528
529        let mut digests_per_block: usize = 1;
530        while digests_per_block < starts.len() {
531            for i in (0..starts.len()).step_by(digests_per_block * 2) {
532                if i + digests_per_block < starts.len() {
533                    let first = starts[i];
534                    let middle = starts[i + digests_per_block];
535                    let last = if i + 2 * digests_per_block < starts.len() {
536                        starts[i + 2 * digests_per_block]
537                    } else {
538                        centroids.len()
539                    };
540
541                    debug_assert!(first <= middle && middle <= last);
542                    Self::external_merge(&mut centroids, first, middle, last);
543                }
544            }
545
546            digests_per_block *= 2;
547        }
548
549        let mut result = TDigest::new_with_size(max_size);
550        let mut compressed: Vec<Centroid> = Vec::with_capacity(max_size);
551
552        let mut k_limit: f64 = 1.0;
553        let mut q_limit_times_count: f64 = Self::k_to_q(k_limit, max_size as f64) * (count as f64);
554
555        let mut iter_centroids = centroids.iter_mut();
556        let mut curr = iter_centroids.next().unwrap();
557        let mut weight_so_far: f64 = curr.weight();
558        let mut sums_to_merge: f64 = 0.0;
559        let mut weights_to_merge: f64 = 0.0;
560
561        for centroid in iter_centroids {
562            weight_so_far += centroid.weight();
563
564            if weight_so_far <= q_limit_times_count {
565                sums_to_merge += centroid.mean() * centroid.weight();
566                weights_to_merge += centroid.weight();
567            } else {
568                result.sum = OrderedFloat::from(result.sum.into_inner() + curr.add(sums_to_merge, weights_to_merge));
569                sums_to_merge = 0.0;
570                weights_to_merge = 0.0;
571                compressed.push(curr.clone());
572                q_limit_times_count = Self::k_to_q(k_limit, max_size as f64) * (count as f64);
573                k_limit += 1.0;
574                curr = centroid;
575            }
576        }
577
578        result.sum = OrderedFloat::from(result.sum.into_inner() + curr.add(sums_to_merge, weights_to_merge));
579        compressed.push(curr.clone());
580        compressed.shrink_to_fit();
581        compressed.sort();
582
583        result.count = OrderedFloat::from(count as f64);
584        result.min = min;
585        result.max = max;
586        result.centroids = compressed;
587        result
588    }
589
590    /// To estimate the value located at `q` quantile
591    pub fn estimate_quantile(&self, q: f64) -> f64 {
592        if self.centroids.is_empty() {
593            return 0.0;
594        }
595
596        let count_: f64 = self.count.into_inner();
597        let rank: f64 = q * count_;
598
599        let mut pos: usize;
600        let mut t: f64;
601        if q > 0.5 {
602            if q >= 1.0 {
603                return self.max();
604            }
605
606            pos = 0;
607            t = count_;
608
609            for (k, centroid) in self.centroids.iter().enumerate().rev() {
610                t -= centroid.weight();
611
612                if rank >= t {
613                    pos = k;
614                    break;
615                }
616            }
617        } else {
618            if q <= 0.0 {
619                return self.min();
620            }
621
622            pos = self.centroids.len() - 1;
623            t = 0.0;
624
625            for (k, centroid) in self.centroids.iter().enumerate() {
626                if rank < t + centroid.weight() {
627                    pos = k;
628                    break;
629                }
630
631                t += centroid.weight();
632            }
633        }
634
635        let mut delta = 0.0;
636        let mut min: f64 = self.min.into_inner();
637        let mut max: f64 = self.max.into_inner();
638
639        if self.centroids.len() > 1 {
640            if pos == 0 {
641                delta = self.centroids[pos + 1].mean() - self.centroids[pos].mean();
642                max = self.centroids[pos + 1].mean();
643            } else if pos == (self.centroids.len() - 1) {
644                delta = self.centroids[pos].mean() - self.centroids[pos - 1].mean();
645                min = self.centroids[pos - 1].mean();
646            } else {
647                delta = (self.centroids[pos + 1].mean() - self.centroids[pos - 1].mean()) / 2.0;
648                min = self.centroids[pos - 1].mean();
649                max = self.centroids[pos + 1].mean();
650            }
651        }
652
653        let value = self.centroids[pos].mean() + ((rank - t) / self.centroids[pos].weight() - 0.5) * delta;
654        Self::clamp(value, min, max)
655    }
656}
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661
662    #[test]
663    fn test_centroid_addition_regression() {
664        //https://github.com/MnO2/t-digest/pull/1
665
666        let vals = vec![1.0, 1.0, 1.0, 2.0, 1.0, 1.0];
667        let mut t = TDigest::new_with_size(10);
668
669        for v in vals {
670            t = t.merge_unsorted(vec![v]);
671        }
672
673        let ans = t.estimate_quantile(0.5);
674        let expected: f64 = 1.0;
675        let percentage: f64 = (expected - ans).abs() / expected;
676        assert!(percentage < 0.01);
677
678        let ans = t.estimate_quantile(0.95);
679        let expected: f64 = 2.0;
680        let percentage: f64 = (expected - ans).abs() / expected;
681        assert!(percentage < 0.01);
682    }
683
684    #[test]
685    fn test_merge_sorted_against_uniform_distro() {
686        let t = TDigest::new_with_size(100);
687        let values: Vec<f64> = (1..=1_000_000).map(f64::from).collect();
688
689        let t = t.merge_sorted(values);
690
691        let ans = t.estimate_quantile(1.0);
692        let expected: f64 = 1_000_000.0;
693
694        let percentage: f64 = (expected - ans).abs() / expected;
695        assert!(percentage < 0.01);
696
697        let ans = t.estimate_quantile(0.99);
698        let expected: f64 = 990_000.0;
699
700        let percentage: f64 = (expected - ans).abs() / expected;
701        assert!(percentage < 0.01);
702
703        let ans = t.estimate_quantile(0.01);
704        let expected: f64 = 10_000.0;
705
706        let percentage: f64 = (expected - ans).abs() / expected;
707        assert!(percentage < 0.01);
708
709        let ans = t.estimate_quantile(0.0);
710        let expected: f64 = 1.0;
711
712        let percentage: f64 = (expected - ans).abs() / expected;
713        assert!(percentage < 0.01);
714
715        let ans = t.estimate_quantile(0.5);
716        let expected: f64 = 500_000.0;
717
718        let percentage: f64 = (expected - ans).abs() / expected;
719        assert!(percentage < 0.01);
720    }
721
722    #[test]
723    fn test_merge_unsorted_against_uniform_distro() {
724        let t = TDigest::new_with_size(100);
725        let values: Vec<f64> = (1..=1_000_000).map(f64::from).collect();
726
727        let t = t.merge_unsorted(values);
728
729        let ans = t.estimate_quantile(1.0);
730        let expected: f64 = 1_000_000.0;
731
732        let percentage: f64 = (expected - ans).abs() / expected;
733        assert!(percentage < 0.01);
734
735        let ans = t.estimate_quantile(0.99);
736        let expected: f64 = 990_000.0;
737
738        let percentage: f64 = (expected - ans).abs() / expected;
739        assert!(percentage < 0.01);
740
741        let ans = t.estimate_quantile(0.01);
742        let expected: f64 = 10_000.0;
743
744        let percentage: f64 = (expected - ans).abs() / expected;
745        assert!(percentage < 0.01);
746
747        let ans = t.estimate_quantile(0.0);
748        let expected: f64 = 1.0;
749
750        let percentage: f64 = (expected - ans).abs() / expected;
751        assert!(percentage < 0.01);
752
753        let ans = t.estimate_quantile(0.5);
754        let expected: f64 = 500_000.0;
755
756        let percentage: f64 = (expected - ans).abs() / expected;
757        assert!(percentage < 0.01);
758    }
759
760    #[test]
761    fn test_merge_sorted_against_skewed_distro() {
762        let t = TDigest::new_with_size(100);
763        let mut values: Vec<f64> = (1..=600_000).map(f64::from).collect();
764        for _ in 0..400_000 {
765            values.push(1_000_000.0);
766        }
767
768        let t = t.merge_sorted(values);
769
770        let ans = t.estimate_quantile(0.99);
771        let expected: f64 = 1_000_000.0;
772        let percentage: f64 = (expected - ans).abs() / expected;
773        assert!(percentage < 0.01);
774
775        let ans = t.estimate_quantile(0.01);
776        let expected: f64 = 10_000.0;
777
778        let percentage: f64 = (expected - ans).abs() / expected;
779        assert!(percentage < 0.01);
780
781        let ans = t.estimate_quantile(0.5);
782        let expected: f64 = 500_000.0;
783
784        let percentage: f64 = (expected - ans).abs() / expected;
785        assert!(percentage < 0.01);
786    }
787
788    #[test]
789    fn test_merge_unsorted_against_skewed_distro() {
790        let t = TDigest::new_with_size(100);
791        let mut values: Vec<f64> = (1..=600_000).map(f64::from).collect();
792        for _ in 0..400_000 {
793            values.push(1_000_000.0);
794        }
795
796        let t = t.merge_unsorted(values);
797
798        let ans = t.estimate_quantile(0.99);
799        let expected: f64 = 1_000_000.0;
800        let percentage: f64 = (expected - ans).abs() / expected;
801        assert!(percentage < 0.01);
802
803        let ans = t.estimate_quantile(0.01);
804        let expected: f64 = 10_000.0;
805
806        let percentage: f64 = (expected - ans).abs() / expected;
807        assert!(percentage < 0.01);
808
809        let ans = t.estimate_quantile(0.5);
810        let expected: f64 = 500_000.0;
811
812        let percentage: f64 = (expected - ans).abs() / expected;
813        assert!(percentage < 0.01);
814    }
815
816    #[test]
817    fn test_merge_digests() {
818        let mut digests: Vec<TDigest> = Vec::new();
819
820        for _ in 1..=100 {
821            let t = TDigest::new_with_size(100);
822            let values: Vec<f64> = (1..=1_000).map(f64::from).collect();
823            let t = t.merge_sorted(values);
824            digests.push(t)
825        }
826
827        let t = TDigest::merge_digests(digests);
828
829        let ans = t.estimate_quantile(1.0);
830        let expected: f64 = 1000.0;
831
832        let percentage: f64 = (expected - ans).abs() / expected;
833        assert!(percentage < 0.01);
834
835        let ans = t.estimate_quantile(0.99);
836        let expected: f64 = 990.0;
837
838        let percentage: f64 = (expected - ans).abs() / expected;
839        assert!(percentage < 0.01);
840
841        let ans = t.estimate_quantile(0.01);
842        let expected: f64 = 10.0;
843
844        let percentage: f64 = (expected - ans).abs() / expected;
845        assert!(percentage < 0.2);
846
847        let ans = t.estimate_quantile(0.0);
848        let expected: f64 = 1.0;
849
850        let percentage: f64 = (expected - ans).abs() / expected;
851        assert!(percentage < 0.01);
852
853        let ans = t.estimate_quantile(0.5);
854        let expected: f64 = 500.0;
855
856        let percentage: f64 = (expected - ans).abs() / expected;
857        assert!(percentage < 0.01);
858    }
859}