dataprof_core/sampling/
reservoir.rs1use rand::{Rng, SeedableRng};
2use rand_chacha::ChaCha8Rng;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
13pub struct ReservoirSampler {
14 capacity: usize,
16 reservoir: Vec<usize>,
18 total_processed: usize,
20 rng: ChaCha8Rng,
22 next_record: usize,
24 stats: ReservoirStats,
26}
27
28#[derive(Debug, Clone, Default)]
30pub struct ReservoirStats {
31 pub records_processed: usize,
32 pub records_sampled: usize,
33 pub replacement_count: usize,
34 pub skip_count: usize,
35 pub efficiency_ratio: f64,
36}
37
38impl ReservoirSampler {
39 pub fn new(capacity: usize) -> Self {
41 Self::seed(capacity, 42) }
43
44 pub fn seed(capacity: usize, seed: u64) -> Self {
46 Self {
47 capacity,
48 reservoir: Vec::with_capacity(capacity),
49 total_processed: 0,
50 rng: ChaCha8Rng::seed_from_u64(seed),
51 next_record: 0,
52 stats: ReservoirStats::default(),
53 }
54 }
55
56 pub fn process_record(&mut self, record_index: usize) -> bool {
59 self.total_processed += 1;
60 self.stats.records_processed += 1;
61
62 if self.reservoir.len() < self.capacity {
64 self.reservoir.push(record_index);
65 self.stats.records_sampled += 1;
66 return true;
67 }
68
69 self.apply_vitter_algorithm(record_index)
71 }
72
73 fn apply_vitter_algorithm(&mut self, record_index: usize) -> bool {
75 if self.total_processed < self.next_record {
77 return false;
78 }
79
80 let random_index = self.rng.random_range(0..self.total_processed);
82
83 if random_index < self.capacity {
84 let replace_position = random_index % self.capacity;
86 self.reservoir[replace_position] = record_index;
87 self.stats.replacement_count += 1;
88 self.stats.records_sampled += 1;
89
90 self.calculate_next_skip();
92
93 return true;
94 }
95
96 false
97 }
98
99 fn calculate_next_skip(&mut self) {
102 let u: f64 = self.rng.random();
105 let skip = if u > 0.0 {
106 ((self.total_processed as f64) * (u.powf(1.0 / self.capacity as f64) - 1.0)) as usize
107 } else {
108 1
109 };
110
111 self.next_record = self.total_processed + skip.max(1);
112 self.stats.skip_count += skip;
113 }
114
115 pub fn replacement_slot(&mut self, seen: usize) -> Option<usize> {
127 if self.capacity == 0 || seen <= self.capacity {
128 return None;
129 }
130 self.total_processed = seen;
131 let draw = self.rng.random_range(0..seen);
132 if draw < self.capacity {
133 self.stats.replacement_count += 1;
134 Some(draw)
135 } else {
136 None
137 }
138 }
139
140 pub fn get_sample_indices(&self) -> &[usize] {
142 &self.reservoir
143 }
144
145 pub fn sample_size(&self) -> usize {
147 self.reservoir.len()
148 }
149
150 pub fn is_full(&self) -> bool {
152 self.reservoir.len() >= self.capacity
153 }
154
155 pub fn get_stats(&self) -> &ReservoirStats {
157 &self.stats
158 }
159
160 pub fn sampling_ratio(&self) -> f64 {
162 if self.total_processed > 0 {
163 self.reservoir.len() as f64 / self.total_processed as f64
164 } else {
165 0.0
166 }
167 }
168
169 pub fn reset(&mut self) {
171 self.reservoir.clear();
172 self.total_processed = 0;
173 self.next_record = 0;
174 self.stats = ReservoirStats::default();
175 }
176
177 pub fn set_seed(&mut self, seed: u64) {
179 self.rng = ChaCha8Rng::seed_from_u64(seed);
180 }
181
182 pub fn update_efficiency_stats(&mut self) {
184 self.stats.efficiency_ratio = if self.stats.records_processed > 0 {
185 self.stats.records_sampled as f64 / self.stats.records_processed as f64
186 } else {
187 0.0
188 };
189 }
190}
191
192#[derive(Debug, Clone)]
194pub struct WeightedReservoirSampler {
195 base_sampler: ReservoirSampler,
196 weights: HashMap<String, f64>,
198 total_weight: f64,
200}
201
202impl WeightedReservoirSampler {
203 pub fn new(capacity: usize, weights: HashMap<String, f64>) -> Self {
204 Self {
205 base_sampler: ReservoirSampler::new(capacity),
206 weights,
207 total_weight: 0.0,
208 }
209 }
210
211 pub fn process_weighted_record(&mut self, record_index: usize, category: &str) -> bool {
213 let weight = self.weights.get(category).copied().unwrap_or(1.0);
214 self.total_weight += weight;
215
216 let adjusted_probability = weight / self.total_weight;
218 let u: f64 = self.base_sampler.rng.random();
219
220 if u < adjusted_probability {
221 self.base_sampler.process_record(record_index)
222 } else {
223 self.base_sampler.total_processed += 1;
224 false
225 }
226 }
227
228 pub fn get_sample_indices(&self) -> &[usize] {
229 self.base_sampler.get_sample_indices()
230 }
231
232 pub fn sampling_ratio(&self) -> f64 {
233 self.base_sampler.sampling_ratio()
234 }
235}
236
237#[derive(Debug)]
239pub struct MultiReservoirSampler {
240 reservoirs: HashMap<String, ReservoirSampler>,
241 default_capacity: usize,
242}
243
244impl MultiReservoirSampler {
245 pub fn new(default_capacity: usize) -> Self {
246 Self {
247 reservoirs: HashMap::new(),
248 default_capacity,
249 }
250 }
251
252 pub fn process_categorized_record(&mut self, record_index: usize, category: &str) -> bool {
254 let reservoir = self
255 .reservoirs
256 .entry(category.to_string())
257 .or_insert_with(|| ReservoirSampler::new(self.default_capacity));
258
259 reservoir.process_record(record_index)
260 }
261
262 pub fn get_combined_sample(&self) -> Vec<usize> {
264 let mut combined = Vec::new();
265
266 for reservoir in self.reservoirs.values() {
267 combined.extend_from_slice(reservoir.get_sample_indices());
268 }
269
270 combined.sort_unstable();
272 combined
273 }
274
275 pub fn get_samples_by_category(&self) -> HashMap<String, Vec<usize>> {
277 self.reservoirs
278 .iter()
279 .map(|(category, reservoir)| {
280 (
281 category.to_string(),
282 reservoir.get_sample_indices().to_vec(),
283 )
284 })
285 .collect()
286 }
287
288 pub fn get_all_stats(&self) -> HashMap<String, ReservoirStats> {
290 self.reservoirs
291 .iter()
292 .map(|(category, reservoir)| (category.to_string(), reservoir.get_stats().clone()))
293 .collect()
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn test_basic_reservoir_sampling() {
303 let mut sampler = ReservoirSampler::new(10);
304
305 let mut selected_count = 0;
307 for i in 0..100 {
308 if sampler.process_record(i) {
309 selected_count += 1;
310 }
311 }
312
313 assert_eq!(sampler.sample_size(), 10);
315 assert_eq!(sampler.get_sample_indices().len(), 10);
316 assert!(selected_count >= 10); }
318
319 #[test]
320 fn test_reservoir_filling_phase() {
321 let mut sampler = ReservoirSampler::new(5);
322
323 for i in 0..5 {
325 assert!(sampler.process_record(i));
326 }
327
328 assert_eq!(sampler.sample_size(), 5);
329 assert!(sampler.is_full());
330 }
331
332 #[test]
333 fn test_replacement_phase() {
334 let mut sampler = ReservoirSampler::seed(3, 42); for i in 0..3 {
338 sampler.process_record(i);
339 }
340
341 let _initial_sample = sampler.get_sample_indices().to_vec();
343
344 for i in 3..20 {
345 sampler.process_record(i);
346 }
347
348 let final_sample = sampler.get_sample_indices().to_vec();
349
350 assert_eq!(final_sample.len(), 3);
352
353 assert!(sampler.get_stats().replacement_count > 0);
355 }
356
357 #[test]
358 fn test_sampling_ratio() {
359 let mut sampler = ReservoirSampler::new(10);
360
361 for i in 0..100 {
362 sampler.process_record(i);
363 }
364
365 let ratio = sampler.sampling_ratio();
366 assert!((ratio - 0.1).abs() < 0.01); }
368
369 #[test]
370 fn test_reset_functionality() {
371 let mut sampler = ReservoirSampler::new(5);
372
373 for i in 0..10 {
374 sampler.process_record(i);
375 }
376
377 assert_eq!(sampler.sample_size(), 5);
378 assert!(sampler.total_processed > 0);
379
380 sampler.reset();
381
382 assert_eq!(sampler.sample_size(), 0);
383 assert_eq!(sampler.total_processed, 0);
384 }
385
386 #[test]
387 fn test_weighted_sampling() {
388 let mut weights = HashMap::new();
389 weights.insert("high".to_string(), 3.0);
390 weights.insert("low".to_string(), 1.0);
391
392 let mut sampler = WeightedReservoirSampler::new(10, weights);
393
394 let mut _high_selected = 0;
395 let mut _low_selected = 0;
396
397 for i in 0..50 {
399 let category = if i % 2 == 0 { "high" } else { "low" };
400 if sampler.process_weighted_record(i, category) {
401 if category == "high" {
402 _high_selected += 1;
403 } else {
404 _low_selected += 1;
405 }
406 }
407 }
408
409 assert!(sampler.get_sample_indices().len() <= 10);
412 }
413
414 #[test]
415 fn test_multi_reservoir() {
416 let mut sampler = MultiReservoirSampler::new(5);
417
418 for i in 0..20 {
419 let category = format!("type_{}", i % 3);
420 sampler.process_categorized_record(i, &category);
421 }
422
423 let combined = sampler.get_combined_sample();
424 assert!(combined.len() <= 15); let by_category = sampler.get_samples_by_category();
427 assert_eq!(by_category.len(), 3); }
429
430 #[test]
431 fn test_deterministic_with_seed() {
432 let mut sampler1 = ReservoirSampler::seed(5, 123);
433 let mut sampler2 = ReservoirSampler::seed(5, 123);
434
435 for i in 0..50 {
436 sampler1.process_record(i);
437 sampler2.process_record(i);
438 }
439
440 assert_eq!(sampler1.get_sample_indices(), sampler2.get_sample_indices());
442 }
443}