1use rand::rngs::SmallRng;
2use rand::{Rng, SeedableRng};
3use std::collections::{HashMap, HashSet};
4use std::fmt::Write as _;
5
6use crate::{ValueHintBindingAccumulator, profile_builder::infer_data_type_streaming};
7use dataprof_core::{SemanticHintBinding, SemanticHintKind, SemanticHints};
8use dataprof_metrics::analysis::inference::is_null_like_token;
9use dataprof_metrics::{
10 CardinalityEstimator, HyperLogLog, RowDuplicateSummary, value_matches_hint,
11};
12
13#[derive(Debug, Clone)]
23pub struct WelfordAccumulator {
24 count: u64,
25 mean: f64,
26 m2: f64,
27}
28
29impl WelfordAccumulator {
30 pub fn new() -> Self {
31 Self {
32 count: 0,
33 mean: 0.0,
34 m2: 0.0,
35 }
36 }
37
38 #[inline]
39 pub fn update(&mut self, value: f64) {
40 self.count += 1;
41 let delta = value - self.mean;
42 self.mean += delta / self.count as f64;
43 let delta2 = value - self.mean;
44 self.m2 += delta * delta2;
45 }
46
47 #[inline]
48 pub fn mean(&self) -> f64 {
49 if self.count == 0 { 0.0 } else { self.mean }
50 }
51
52 #[inline]
54 pub fn count(&self) -> u64 {
55 self.count
56 }
57
58 pub fn variance(&self) -> f64 {
59 if self.count < 2 {
60 0.0
61 } else {
62 self.m2 / self.count as f64
63 }
64 }
65
66 pub fn std_dev(&self) -> f64 {
67 self.variance().sqrt()
68 }
69
70 pub fn sample_variance(&self) -> f64 {
73 if self.count < 2 {
74 0.0
75 } else {
76 (self.m2 / (self.count - 1) as f64).max(0.0)
77 }
78 }
79
80 pub fn sample_std_dev(&self) -> f64 {
82 self.sample_variance().sqrt()
83 }
84
85 pub fn merge(&mut self, other: &WelfordAccumulator) {
86 if other.count == 0 {
87 return;
88 }
89 if self.count == 0 {
90 *self = other.clone();
91 return;
92 }
93
94 let combined_count = self.count + other.count;
95 let delta = other.mean - self.mean;
96 let new_mean = self.mean + delta * (other.count as f64 / combined_count as f64);
97 let new_m2 = self.m2
98 + other.m2
99 + delta * delta * (self.count as f64 * other.count as f64 / combined_count as f64);
100
101 self.count = combined_count;
102 self.mean = new_mean;
103 self.m2 = new_m2;
104 }
105}
106
107impl Default for WelfordAccumulator {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113#[derive(Debug, Clone)]
114pub struct StreamReservoirSampler {
115 reservoir: Vec<String>,
116 capacity: usize,
117 count: u64,
118 rng: SmallRng,
119}
120
121impl StreamReservoirSampler {
122 const DEFAULT_SEED: u64 = 0xDA7A_900D_F00D_5EED;
123
124 pub fn new(capacity: usize) -> Self {
125 let capacity = capacity.max(1);
126 Self {
127 reservoir: Vec::with_capacity(capacity.min(1024)),
128 capacity,
129 count: 0,
130 rng: SmallRng::seed_from_u64(Self::DEFAULT_SEED),
134 }
135 }
136
137 #[cfg(test)]
138 pub fn seed(capacity: usize, seed: u64) -> Self {
139 Self {
140 reservoir: Vec::with_capacity(capacity.min(1024)),
141 capacity,
142 count: 0,
143 rng: SmallRng::seed_from_u64(seed),
144 }
145 }
146
147 #[inline]
148 pub fn offer(&mut self, value: String) {
149 self.count += 1;
150 if self.reservoir.len() < self.capacity {
151 self.reservoir.push(value);
152 } else {
153 let index = self.rng.random_range(0..self.count as usize);
154 if index < self.capacity {
155 self.reservoir[index] = value;
156 }
157 }
158 }
159
160 pub fn shrink_to(&mut self, new_capacity: usize) {
161 let new_capacity = new_capacity.max(1);
162 self.capacity = new_capacity;
163 self.reservoir.truncate(new_capacity);
164 self.reservoir.shrink_to_fit();
165 }
166
167 pub fn samples(&self) -> &[String] {
168 &self.reservoir
169 }
170
171 pub fn memory_usage_bytes(&self) -> usize {
172 self.reservoir
173 .iter()
174 .map(|value| std::mem::size_of::<String>() + value.capacity())
175 .sum()
176 }
177
178 pub fn merge(&mut self, other: &StreamReservoirSampler) {
179 if other.count == 0 {
180 return;
181 }
182
183 let mut combined: Vec<String> = self.reservoir.drain(..).collect();
184 combined.extend(other.reservoir.iter().cloned());
185
186 let total = combined.len();
187 if total <= self.capacity {
188 self.reservoir = combined;
189 } else {
190 for index in 0..self.capacity {
191 let swap_with = self.rng.random_range(index..total);
192 combined.swap(index, swap_with);
193 }
194 combined.truncate(self.capacity);
195 self.reservoir = combined;
196 }
197
198 self.count += other.count;
199 }
200}
201
202#[derive(Debug, Clone)]
203pub struct TextLengthStats {
204 pub min_length: usize,
205 pub max_length: usize,
206 pub avg_length: f64,
207 welford: WelfordAccumulator,
208 histogram: [u64; 32],
209}
210
211impl TextLengthStats {
212 pub fn new() -> Self {
213 Self {
214 min_length: usize::MAX,
215 max_length: 0,
216 avg_length: 0.0,
217 welford: WelfordAccumulator::new(),
218 histogram: [0u64; 32],
219 }
220 }
221
222 pub fn update(&mut self, length: usize) {
223 self.min_length = self.min_length.min(length);
224 self.max_length = self.max_length.max(length);
225 self.welford.update(length as f64);
226 self.avg_length = self.welford.mean();
227
228 let bucket = if length == 0 {
229 0
230 } else {
231 (usize::BITS - length.leading_zeros()).min(31) as usize
232 };
233 self.histogram[bucket] += 1;
234 }
235
236 pub fn merge(&mut self, other: &TextLengthStats) {
237 if other.welford.count == 0 {
238 return;
239 }
240 if self.welford.count == 0 {
241 *self = other.clone();
242 return;
243 }
244
245 self.min_length = self.min_length.min(other.min_length);
246 self.max_length = self.max_length.max(other.max_length);
247 self.welford.merge(&other.welford);
248 self.avg_length = self.welford.mean();
249
250 for (left, right) in self.histogram.iter_mut().zip(other.histogram.iter()) {
251 *left += *right;
252 }
253 }
254
255 pub fn empty() -> Self {
256 Self {
257 min_length: 0,
258 max_length: 0,
259 avg_length: 0.0,
260 welford: WelfordAccumulator::new(),
261 histogram: [0u64; 32],
262 }
263 }
264}
265
266impl Default for TextLengthStats {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272#[derive(Debug, Clone)]
273pub struct StreamingStatistics {
274 pub count: usize,
275 pub null_count: usize,
276 pub min: f64,
277 pub max: f64,
278 welford: WelfordAccumulator,
279 hll: HyperLogLog,
280 sampler: StreamReservoirSampler,
281 text_length_tracker: TextLengthStats,
282 date_match_count: usize,
283}
284
285impl StreamingStatistics {
286 pub fn new() -> Self {
287 Self {
288 count: 0,
289 null_count: 0,
290 min: f64::INFINITY,
291 max: f64::NEG_INFINITY,
292 welford: WelfordAccumulator::new(),
293 hll: HyperLogLog::new(),
294 sampler: StreamReservoirSampler::new(10_000),
295 text_length_tracker: TextLengthStats::new(),
296 date_match_count: 0,
297 }
298 }
299
300 pub fn with_sample_capacity(max_sample: usize) -> Self {
301 Self {
302 sampler: StreamReservoirSampler::new(max_sample),
303 ..Self::new()
304 }
305 }
306
307 pub fn update(&mut self, value: &str) {
308 self.count += 1;
309
310 if is_null_like_token(value) {
311 self.null_count += 1;
312 return;
313 }
314
315 self.hll.insert(value);
316 self.sampler.offer(value.to_string());
317 self.text_length_tracker.update(value.len());
318 if value_matches_hint(value, SemanticHintKind::Temporal) {
319 self.date_match_count += 1;
320 }
321
322 if let Some(number) = value.parse::<f64>().ok().filter(|num| num.is_finite()) {
323 self.welford.update(number);
324 self.min = self.min.min(number);
325 self.max = self.max.max(number);
326 }
327 }
328
329 pub fn merge(&mut self, other: &StreamingStatistics) {
330 self.count += other.count;
331 self.null_count += other.null_count;
332
333 if other.min < self.min {
334 self.min = other.min;
335 }
336 if other.max > self.max {
337 self.max = other.max;
338 }
339
340 self.welford.merge(&other.welford);
341 self.hll.merge(&other.hll);
342 self.sampler.merge(&other.sampler);
343 self.text_length_tracker.merge(&other.text_length_tracker);
344 self.date_match_count += other.date_match_count;
345 }
346
347 pub fn mean(&self) -> f64 {
348 self.welford.mean()
349 }
350
351 pub fn variance(&self) -> f64 {
352 self.welford.variance()
353 }
354
355 pub fn std_dev(&self) -> f64 {
356 self.welford.std_dev()
357 }
358
359 pub fn unique_count(&self) -> usize {
360 if !self.unique_count_is_approximate() {
361 return self.sampler.samples().iter().collect::<HashSet<_>>().len();
362 }
363 self.hll.count() as usize
364 }
365
366 pub fn unique_count_is_approximate(&self) -> bool {
367 (self.sampler.samples().len() as u64) < self.sampler.count
368 }
369
370 pub fn sample_values(&self) -> &[String] {
371 self.sampler.samples()
372 }
373
374 pub fn date_match_count(&self) -> usize {
376 self.date_match_count
377 }
378
379 pub fn exact_numeric_aggregates(
386 &self,
387 ) -> Option<crate::profile_builder::ExactNumericAggregates> {
388 let count = self.welford.count();
389 if count == 0 {
390 return None;
391 }
392 Some(crate::profile_builder::ExactNumericAggregates {
393 min: self.min,
394 max: self.max,
395 mean: self.welford.mean(),
396 std_dev: self.welford.sample_std_dev(),
397 variance: self.welford.sample_variance(),
398 count: count as usize,
399 })
400 }
401
402 pub fn text_length_stats(&self) -> TextLengthStats {
403 if self.text_length_tracker.welford.count == 0 {
404 return TextLengthStats::empty();
405 }
406 self.text_length_tracker.clone()
407 }
408
409 pub fn reduce_sample_capacity(&mut self) {
410 self.sampler.shrink_to(self.sampler.capacity / 2);
411 }
412
413 pub fn memory_usage_bytes(&self) -> usize {
414 let struct_size = std::mem::size_of::<Self>();
415 let hll_size = self.hll.memory_usage_bytes();
416 let reservoir_size = self.sampler.memory_usage_bytes();
417
418 struct_size + hll_size + reservoir_size
419 }
420}
421
422impl Default for StreamingStatistics {
423 fn default() -> Self {
424 Self::new()
425 }
426}
427
428#[derive(Debug, Clone, Default)]
437pub struct RowUniquenessTracker {
438 rows_seen: usize,
439 distinct: CardinalityEstimator,
440}
441
442impl RowUniquenessTracker {
443 pub fn observe(&mut self, signature: String) {
444 self.rows_seen += 1;
445 self.distinct.insert_owned(signature);
446 }
447
448 pub fn rows_seen(&self) -> usize {
449 self.rows_seen
450 }
451
452 pub fn duplicate_rows(&self) -> usize {
454 self.rows_seen.saturating_sub(self.distinct.estimate())
455 }
456
457 pub fn is_approximate(&self) -> bool {
458 self.distinct.is_approximate()
459 }
460
461 pub fn merge(&mut self, other: &RowUniquenessTracker) {
462 self.rows_seen += other.rows_seen;
463 self.distinct.merge(&other.distinct);
464 }
465
466 pub fn memory_usage_bytes(&self) -> usize {
467 self.distinct.memory_usage_bytes()
468 }
469
470 pub fn summary(&self) -> Option<RowDuplicateSummary> {
473 if self.rows_seen == 0 {
474 return None;
475 }
476 Some(RowDuplicateSummary {
477 duplicate_rows: self.duplicate_rows(),
478 rows_checked: self.rows_seen,
479 approximate: self.is_approximate(),
480 })
481 }
482}
483
484pub struct StreamingColumnCollection {
485 columns: HashMap<String, StreamingStatistics>,
486 ordered_names: Vec<String>,
487 memory_limit_bytes: usize,
488 row_tracker: RowUniquenessTracker,
489 hint_bindings: ValueHintBindingAccumulator,
490}
491
492impl StreamingColumnCollection {
493 pub fn new() -> Self {
494 Self {
495 columns: HashMap::new(),
496 ordered_names: Vec::new(),
497 memory_limit_bytes: 100 * 1024 * 1024,
498 row_tracker: RowUniquenessTracker::default(),
499 hint_bindings: ValueHintBindingAccumulator::default(),
500 }
501 }
502
503 pub fn memory_limit(limit_mb: usize) -> Self {
504 Self {
505 columns: HashMap::new(),
506 ordered_names: Vec::new(),
507 memory_limit_bytes: limit_mb * 1024 * 1024,
508 row_tracker: RowUniquenessTracker::default(),
509 hint_bindings: ValueHintBindingAccumulator::default(),
510 }
511 }
512
513 pub fn with_semantic_hints(mut self, hints: &SemanticHints) -> Self {
515 self.hint_bindings = ValueHintBindingAccumulator::new(hints);
516 self
517 }
518
519 pub fn init_columns(&mut self, headers: &[String]) {
520 for header in headers {
521 if !self.columns.contains_key(header) {
522 self.columns
523 .insert(header.clone(), StreamingStatistics::default());
524 self.ordered_names.push(header.clone());
525 }
526 }
527 }
528
529 pub fn init_column_with_missing(&mut self, header: &str, prior_rows: usize) {
536 if self.columns.contains_key(header) {
537 return;
538 }
539
540 let stats = StreamingStatistics {
541 count: prior_rows,
542 null_count: prior_rows,
543 ..Default::default()
544 };
545 self.columns.insert(header.to_string(), stats);
546 self.ordered_names.push(header.to_string());
547 }
548
549 pub fn process_record<I>(&mut self, headers: &[String], values: I)
550 where
551 I: IntoIterator<Item = String>,
552 {
553 let mut row_signature = String::new();
556 let mut values = values.into_iter();
557
558 for header in headers {
562 let value = values.next().unwrap_or_default();
563 let _ = write!(row_signature, "{}:", value.len());
564 row_signature.push_str(&value);
565
566 if !self.columns.contains_key(header) {
567 self.ordered_names.push(header.clone());
568 }
569 let stats = self.columns.entry(header.to_string()).or_default();
570 stats.update(&value);
571 self.hint_bindings.observe(header, &value);
572 }
573
574 if !headers.is_empty() {
575 self.row_tracker.observe(row_signature);
576 }
577 }
578
579 pub fn row_duplicate_summary(&self) -> Option<RowDuplicateSummary> {
581 self.row_tracker.summary()
582 }
583
584 pub fn semantic_hint_bindings(&self) -> Vec<SemanticHintBinding> {
586 self.hint_bindings
587 .bindings(self.ordered_names.iter().map(String::as_str))
588 }
589
590 pub fn get_column_stats(&self, column_name: &str) -> Option<&StreamingStatistics> {
591 self.columns.get(column_name)
592 }
593
594 pub fn column_names(&self) -> Vec<String> {
595 self.ordered_names.clone()
596 }
597
598 pub fn memory_usage_bytes(&self) -> usize {
599 self.columns
600 .values()
601 .map(|stats| stats.memory_usage_bytes())
602 .sum::<usize>()
603 + self.row_tracker.memory_usage_bytes()
604 }
605
606 pub fn is_memory_pressure(&self) -> bool {
607 self.memory_usage_bytes() > (self.memory_limit_bytes * 80 / 100)
608 }
609
610 pub fn reduce_memory_usage(&mut self) {
611 for stats in self.columns.values_mut() {
612 stats.reduce_sample_capacity();
613 }
614 }
615
616 pub fn column_type_fingerprint(&self) -> u64 {
621 use std::collections::hash_map::DefaultHasher;
622 use std::hash::{Hash, Hasher};
623
624 let mut hasher = DefaultHasher::new();
625 let mut names: Vec<&String> = self.columns.keys().collect();
626 names.sort();
627 for name in names {
628 let stats = &self.columns[name];
629 let data_type = infer_data_type_streaming(stats);
630 name.hash(&mut hasher);
631 std::mem::discriminant(&data_type).hash(&mut hasher);
632 }
633 hasher.finish()
634 }
635
636 pub fn merge(&mut self, other: StreamingColumnCollection) {
637 for (column_name, other_stats) in other.columns {
638 match self.columns.get_mut(&column_name) {
639 Some(existing_stats) => existing_stats.merge(&other_stats),
640 None => {
641 self.columns.insert(column_name, other_stats);
642 }
643 }
644 }
645 self.row_tracker.merge(&other.row_tracker);
646 self.hint_bindings.merge(&other.hint_bindings);
647 }
648}
649
650impl Default for StreamingColumnCollection {
651 fn default() -> Self {
652 Self::new()
653 }
654}
655
656#[cfg(test)]
657mod row_tracker_tests {
658 use super::*;
659
660 fn record(collection: &mut StreamingColumnCollection, headers: &[String], values: &[&str]) {
661 collection.process_record(headers, values.iter().map(|v| v.to_string()));
662 }
663
664 #[test]
665 fn test_exact_duplicates_including_null_rows() {
666 let headers = vec!["a".to_string(), "b".to_string()];
667 let mut collection = StreamingColumnCollection::new();
668 record(&mut collection, &headers, &["x", ""]);
669 record(&mut collection, &headers, &["x", ""]);
670 record(&mut collection, &headers, &["x", "1"]);
671 record(&mut collection, &headers, &["", ""]);
672
673 let summary = collection
674 .row_duplicate_summary()
675 .expect("rows were observed");
676 assert_eq!(summary.rows_checked, 4);
677 assert_eq!(summary.duplicate_rows, 1);
680 assert!(!summary.approximate);
681 }
682
683 #[test]
684 fn test_field_boundaries_are_unambiguous() {
685 let headers = vec!["a".to_string(), "b".to_string()];
686 let mut collection = StreamingColumnCollection::new();
687 record(&mut collection, &headers, &["ab", "c"]);
688 record(&mut collection, &headers, &["a", "bc"]);
689
690 let summary = collection
691 .row_duplicate_summary()
692 .expect("rows were observed");
693 assert_eq!(
694 summary.duplicate_rows, 0,
695 "different field splits must not collide"
696 );
697 }
698
699 #[test]
700 fn test_ragged_rows_normalize_missing_trailing_fields() {
701 let headers = vec!["a".to_string(), "b".to_string()];
702 let mut collection = StreamingColumnCollection::new();
703 record(&mut collection, &headers, &["x"]);
704 record(&mut collection, &headers, &["x", ""]);
705
706 let summary = collection
707 .row_duplicate_summary()
708 .expect("rows were observed");
709 assert_eq!(summary.rows_checked, 2);
710 assert_eq!(summary.duplicate_rows, 1);
711 assert_eq!(
712 collection
713 .get_column_stats("b")
714 .expect("column b")
715 .null_count,
716 2
717 );
718 }
719
720 #[test]
721 fn test_no_rows_means_no_summary() {
722 let collection = StreamingColumnCollection::new();
723 assert!(collection.row_duplicate_summary().is_none());
724 }
725
726 #[test]
727 fn test_spills_to_approximate_beyond_distinct_threshold() {
728 let headers = vec!["n".to_string()];
729 let mut collection = StreamingColumnCollection::new();
730 let distinct = dataprof_metrics::EXACT_CARDINALITY_THRESHOLD + 500;
731 for i in 0..distinct {
732 record(&mut collection, &headers, &[&i.to_string()]);
733 }
734 for i in 0..distinct {
736 record(&mut collection, &headers, &[&i.to_string()]);
737 }
738
739 let summary = collection
740 .row_duplicate_summary()
741 .expect("rows were observed");
742 assert!(summary.approximate, "past the threshold the count is HLL");
743 assert_eq!(summary.rows_checked, distinct * 2);
744 let error = (summary.duplicate_rows as f64 - distinct as f64).abs() / distinct as f64;
745 assert!(
746 error < 0.05,
747 "estimated {} duplicates for {distinct} true, off by {error:.4}",
748 summary.duplicate_rows
749 );
750 }
751
752 #[test]
753 fn test_merge_combines_row_trackers() {
754 let headers = vec!["a".to_string()];
755 let mut left = StreamingColumnCollection::new();
756 let mut right = StreamingColumnCollection::new();
757 record(&mut left, &headers, &["x"]);
758 record(&mut left, &headers, &["y"]);
759 record(&mut right, &headers, &["x"]);
760
761 left.merge(right);
762 let summary = left.row_duplicate_summary().expect("rows were observed");
763 assert_eq!(summary.rows_checked, 3);
764 assert_eq!(summary.duplicate_rows, 1);
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771
772 #[test]
773 fn test_streaming_statistics() {
774 let mut stats = StreamingStatistics::new();
775
776 stats.update("10.5");
777 stats.update("20.0");
778 stats.update("15.5");
779 stats.update("");
780
781 assert_eq!(stats.count, 4);
782 assert_eq!(stats.null_count, 1);
783 assert_eq!(stats.unique_count(), 3);
784 assert!(!stats.unique_count_is_approximate());
785 assert!((stats.mean() - 15.333333333333334).abs() < 1e-10);
786 assert_eq!(stats.min, 10.5);
787 assert_eq!(stats.max, 20.0);
788 }
789
790 #[test]
791 fn test_streaming_statistics_merge() {
792 let mut stats1 = StreamingStatistics::new();
793 stats1.update("10");
794 stats1.update("20");
795
796 let mut stats2 = StreamingStatistics::new();
797 stats2.update("30");
798 stats2.update("40");
799
800 stats1.merge(&stats2);
801
802 assert_eq!(stats1.count, 4);
803 assert_eq!(stats1.unique_count(), 4);
804 assert!(!stats1.unique_count_is_approximate());
805 assert!((stats1.mean() - 25.0).abs() < 1e-10);
806 assert_eq!(stats1.min, 10.0);
807 assert_eq!(stats1.max, 40.0);
808 }
809
810 #[test]
811 fn test_column_collection() {
812 let mut collection = StreamingColumnCollection::new();
813 let headers = vec!["name".to_string(), "age".to_string()];
814
815 collection.process_record(&headers, vec!["Alice".to_string(), "25".to_string()]);
816 collection.process_record(&headers, vec!["Bob".to_string(), "30".to_string()]);
817
818 let age_stats = collection.get_column_stats("age").unwrap();
819 assert_eq!(age_stats.count, 2);
820 assert!((age_stats.mean() - 27.5).abs() < 1e-10);
821 }
822
823 #[test]
824 fn test_unique_count_becomes_approximate_only_after_reservoir_truncation() {
825 let mut stats = StreamingStatistics::with_sample_capacity(2);
826 stats.update("a");
827 stats.update("b");
828 assert_eq!(stats.unique_count(), 2);
829 assert!(!stats.unique_count_is_approximate());
830
831 stats.update("c");
832 assert!(stats.unique_count_is_approximate());
833 }
834
835 #[test]
836 fn test_default_reservoir_sampling_is_deterministic() {
837 let mut left = StreamReservoirSampler::new(10);
838 let mut right = StreamReservoirSampler::new(10);
839 for value in 0..1_000 {
840 left.offer(value.to_string());
841 right.offer(value.to_string());
842 }
843
844 assert_eq!(left.samples(), right.samples());
845 }
846
847 #[test]
848 fn test_reservoir_zero_capacity_still_retains_a_sample() {
849 let mut sampler = StreamReservoirSampler::new(0);
850 sampler.offer("value".to_string());
851
852 assert_eq!(sampler.samples(), ["value"]);
853 }
854
855 #[test]
856 fn test_late_column_is_backfilled_as_missing() {
857 let mut collection = StreamingColumnCollection::new();
858 collection.init_column_with_missing("late", 3);
859 collection.process_record(&["late".to_string()], ["value".to_string()]);
860
861 let stats = collection.get_column_stats("late").unwrap();
862 assert_eq!(stats.count, 4);
863 assert_eq!(stats.null_count, 3);
864 assert_eq!(stats.unique_count(), 1);
865 }
866
867 #[test]
868 fn test_welford_accuracy() {
869 let mut accumulator = WelfordAccumulator::new();
870 for value in 1..=1000 {
871 accumulator.update(value as f64);
872 }
873 let expected_mean = 500.5;
874 let expected_variance = (1000.0 * 1000.0 - 1.0) / 12.0;
875 assert!((accumulator.mean() - expected_mean).abs() < 1e-6);
876 assert!((accumulator.variance() - expected_variance).abs() < 1.0);
877 }
878
879 #[test]
880 fn test_welford_merge() {
881 let mut left = WelfordAccumulator::new();
882 let mut right = WelfordAccumulator::new();
883 let mut full = WelfordAccumulator::new();
884
885 for value in 1..=500 {
886 left.update(value as f64);
887 full.update(value as f64);
888 }
889 for value in 501..=1000 {
890 right.update(value as f64);
891 full.update(value as f64);
892 }
893
894 left.merge(&right);
895 assert!((left.mean() - full.mean()).abs() < 1e-10);
896 assert!((left.variance() - full.variance()).abs() < 1e-6);
897 }
898
899 #[test]
900 fn test_hll_cardinality() {
901 let mut counter = HyperLogLog::new();
902 let total = 100_000;
903 for index in 0..total {
904 counter.insert(&format!("item_{index}"));
905 }
906 let estimate = counter.count();
907 let error = (estimate as f64 - total as f64).abs() / total as f64;
908 assert!(error < 0.05);
909 }
910
911 #[test]
912 fn test_reservoir_uniformity() {
913 let mut sampler = StreamReservoirSampler::seed(1000, 42);
914 let total = 100_000;
915 for index in 0..total {
916 sampler.offer(index.to_string());
917 }
918
919 assert_eq!(sampler.samples().len(), 1000);
920 let values: Vec<usize> = sampler
921 .samples()
922 .iter()
923 .map(|value| value.parse().unwrap())
924 .collect();
925 let max_value = *values.iter().max().unwrap();
926 assert!(max_value > total / 2);
927 }
928
929 #[test]
930 fn test_text_length_stats_streaming() {
931 let mut stats = TextLengthStats::new();
932 for &length in &[3, 5, 10, 1, 7] {
933 stats.update(length);
934 }
935 assert_eq!(stats.min_length, 1);
936 assert_eq!(stats.max_length, 10);
937 assert!((stats.avg_length - 5.2).abs() < 1e-10);
938 }
939
940 #[test]
941 fn test_memory_usage_bounded() {
942 let mut stats = StreamingStatistics::new();
943 for index in 0..50_000 {
944 stats.update(&format!("value_{index}"));
945 }
946 let usage = stats.memory_usage_bytes();
947 assert!(usage < 1_000_000);
948 }
949}