dataprof_core/sampling/
sampler.rs1use crate::errors::DataProfilerError;
19
20use super::reservoir::ReservoirSampler;
21use super::strategies::SamplingStrategy;
22
23#[derive(Debug)]
25struct SampleBuffer<R> {
26 capacity: usize,
27 rows: Vec<R>,
28 sampler: ReservoirSampler,
29 seen: usize,
30}
31
32impl<R> SampleBuffer<R> {
33 fn new(capacity: usize) -> Self {
34 Self {
35 capacity,
36 rows: Vec::new(),
37 sampler: ReservoirSampler::new(capacity),
38 seen: 0,
39 }
40 }
41
42 fn offer(&mut self, values: R) {
49 self.seen += 1;
50 if self.capacity == 0 {
51 return;
52 }
53 if self.rows.len() < self.capacity {
54 self.rows.push(values);
55 return;
56 }
57 if let Some(position) = self.sampler.replacement_slot(self.seen) {
58 self.rows[position] = values;
59 }
60 }
61
62 fn take(&mut self) -> Vec<R> {
63 std::mem::take(&mut self.rows)
64 }
65}
66
67#[derive(Debug)]
77pub struct RowSampler<R = Vec<String>> {
78 interval: Option<usize>,
80 buffer: Option<SampleBuffer<R>>,
82 iterated: usize,
84 accepted: usize,
86}
87
88impl RowSampler {
89 pub fn new(strategy: &SamplingStrategy) -> Result<Self, DataProfilerError> {
91 Self::for_rows(strategy)
92 }
93}
94
95impl<R> RowSampler<R> {
96 pub fn for_rows(strategy: &SamplingStrategy) -> Result<Self, DataProfilerError> {
99 let (interval, buffer) = match strategy {
100 SamplingStrategy::None => (None, None),
101 SamplingStrategy::Random { size } | SamplingStrategy::Reservoir { size } => {
102 (None, Some(SampleBuffer::new(*size)))
103 }
104 SamplingStrategy::Systematic { interval } => {
106 ((*interval > 1).then_some(*interval), None)
107 }
108 };
109 Ok(Self {
110 interval,
111 buffer,
112 iterated: 0,
113 accepted: 0,
114 })
115 }
116
117 pub fn is_buffered(&self) -> bool {
120 self.buffer.is_some()
121 }
122
123 pub fn is_noop(&self) -> bool {
125 self.interval.is_none() && self.buffer.is_none()
126 }
127
128 pub fn accept(&mut self) -> bool {
133 let index = self.iterated;
134 self.iterated += 1;
135 if self
136 .interval
137 .is_some_and(|interval| !index.is_multiple_of(interval))
138 {
139 return false;
140 }
141 if self.buffer.is_none() {
142 self.accepted += 1;
143 }
144 true
145 }
146
147 pub fn offer(&mut self, values: R) {
151 if let Some(buffer) = self.buffer.as_mut() {
152 buffer.offer(values);
153 }
154 }
155
156 pub fn take_sample(&mut self) -> Vec<R> {
159 match self.buffer.as_mut() {
160 Some(buffer) => {
161 let rows = buffer.take();
162 self.accepted += rows.len();
163 rows
164 }
165 None => Vec::new(),
166 }
167 }
168
169 pub fn iterated_rows(&self) -> usize {
171 self.iterated
172 }
173
174 pub fn sampled_rows(&self) -> usize {
176 self.accepted
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 fn run(strategy: SamplingStrategy, rows: usize) -> (usize, usize) {
185 let mut sampler = RowSampler::new(&strategy).expect("valid strategy");
186 for i in 0..rows {
187 if sampler.accept() && sampler.is_buffered() {
188 sampler.offer(vec![i.to_string()]);
189 }
190 }
191 let final_rows = sampler.take_sample().len();
192 (sampler.sampled_rows(), final_rows)
193 }
194
195 #[test]
196 fn reservoir_yields_exactly_its_size() {
197 let (sampled, buffered) = run(SamplingStrategy::Reservoir { size: 10 }, 100);
198 assert_eq!(buffered, 10);
199 assert_eq!(sampled, 10);
200 }
201
202 #[test]
203 fn reservoir_smaller_than_its_size_keeps_every_row() {
204 let (sampled, buffered) = run(SamplingStrategy::Reservoir { size: 50 }, 20);
205 assert_eq!(buffered, 20, "a short stream cannot fill the reservoir");
206 assert_eq!(sampled, 20);
207 }
208
209 #[test]
210 fn random_matches_reservoir_semantics() {
211 let (_, buffered) = run(SamplingStrategy::Random { size: 25 }, 500);
212 assert_eq!(buffered, 25);
213 }
214
215 #[test]
216 fn systematic_takes_every_nth_row() {
217 let (sampled, _) = run(SamplingStrategy::Systematic { interval: 10 }, 100);
218 assert_eq!(sampled, 10);
219 }
220
221 #[test]
222 fn systematic_with_interval_zero_or_one_keeps_every_row() {
223 for interval in [0, 1] {
224 let (sampled, _) = run(SamplingStrategy::Systematic { interval }, 37);
225 assert_eq!(sampled, 37, "interval {interval}");
226 }
227 }
228
229 #[test]
230 fn none_is_a_noop() {
231 let sampler = RowSampler::new(&SamplingStrategy::None).unwrap();
232 assert!(sampler.is_noop());
233 assert!(!sampler.is_buffered());
234 }
235}