use rand::{prelude::IndexedRandom, Rng, RngExt};
#[derive(Debug, Clone)]
pub struct SamplingConfig {
pub sample_size: SampleSize,
pub method: SamplingMethod,
pub confidence_level: f64,
}
impl Default for SamplingConfig {
fn default() -> Self {
Self {
sample_size: SampleSize::Adaptive,
method: SamplingMethod::Random,
confidence_level: 0.95,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SampleSize {
RowCount(usize),
Percentage(f64),
Adaptive,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SamplingMethod {
Random,
Systematic,
Reservoir,
}
impl SamplingConfig {
pub fn new(sample_size: SampleSize, method: SamplingMethod) -> Self {
Self { sample_size, method, confidence_level: 0.95 }
}
pub fn adaptive() -> Self {
Self::default()
}
pub fn percentage(pct: f64) -> Self {
Self {
sample_size: SampleSize::Percentage(pct),
method: SamplingMethod::Random,
confidence_level: 0.95,
}
}
pub fn fixed(count: usize) -> Self {
Self {
sample_size: SampleSize::RowCount(count),
method: SamplingMethod::Random,
confidence_level: 0.95,
}
}
pub fn determine_sample_size(&self, total_rows: usize) -> (usize, bool) {
match &self.sample_size {
SampleSize::RowCount(count) => {
let sample_size = (*count).min(total_rows);
(sample_size, sample_size < total_rows)
}
SampleSize::Percentage(pct) => {
let sample_size = (total_rows as f64 * pct).ceil() as usize;
let sample_size = sample_size.min(total_rows).max(1);
(sample_size, sample_size < total_rows)
}
SampleSize::Adaptive => {
if total_rows < 1000 {
(total_rows, false)
} else if total_rows < 100_000 {
let sample_size = (total_rows / 10).max(1000);
(sample_size, true)
} else {
(10_000, true)
}
}
}
}
}
pub fn sample_rows<T: Clone>(rows: &[T], config: &SamplingConfig, rng: &mut impl Rng) -> Vec<T> {
let (sample_size, should_sample) = config.determine_sample_size(rows.len());
if !should_sample {
return rows.to_vec();
}
match config.method {
SamplingMethod::Random => random_sample(rows, sample_size, rng),
SamplingMethod::Systematic => systematic_sample(rows, sample_size),
SamplingMethod::Reservoir => reservoir_sample(rows, sample_size, rng),
}
}
fn random_sample<T: Clone>(rows: &[T], n: usize, rng: &mut impl Rng) -> Vec<T> {
let n = n.min(rows.len());
let indices: Vec<usize> = (0..rows.len()).collect();
let sampled_indices = indices.choose_multiple(rng, n);
sampled_indices.map(|&i| rows[i].clone()).collect()
}
fn systematic_sample<T: Clone>(rows: &[T], n: usize) -> Vec<T> {
let n = n.min(rows.len());
if n == 0 {
return Vec::new();
}
let step = rows.len() / n;
if step == 0 {
return rows.to_vec();
}
rows.iter().step_by(step).take(n).cloned().collect()
}
fn reservoir_sample<T: Clone>(rows: &[T], k: usize, rng: &mut impl Rng) -> Vec<T> {
let k = k.min(rows.len());
if k == 0 {
return Vec::new();
}
let mut reservoir: Vec<T> = rows.iter().take(k).cloned().collect();
for (i, row) in rows.iter().enumerate().skip(k) {
let j = rng.random_range(0..=i);
if j < k {
reservoir[j] = row.clone();
}
}
reservoir
}
#[derive(Debug, Clone)]
pub struct SampleMetadata {
pub total_rows: usize,
pub sample_size: usize,
pub sampling_ratio: f64,
pub sampled: bool,
pub confidence_level: f64,
}
impl SampleMetadata {
pub fn new(
total_rows: usize,
sample_size: usize,
sampled: bool,
confidence_level: f64,
) -> Self {
let sampling_ratio =
if total_rows > 0 { sample_size as f64 / total_rows as f64 } else { 1.0 };
Self { total_rows, sample_size, sampling_ratio, sampled, confidence_level }
}
pub fn extrapolate_count(&self, sample_count: usize) -> usize {
if !self.sampled || self.sampling_ratio == 0.0 {
return sample_count;
}
(sample_count as f64 / self.sampling_ratio).round() as usize
}
pub fn extrapolate_frequency(&self, sample_frequency: f64) -> f64 {
if !self.sampled {
return sample_frequency;
}
if self.sample_size < 100 {
let smoothing = 1.0 / self.sample_size as f64;
(sample_frequency + smoothing) / (1.0 + smoothing)
} else {
sample_frequency
}
}
pub fn standard_error(&self, proportion: f64) -> f64 {
if !self.sampled || self.sample_size == 0 {
return 0.0;
}
let fpc = if self.total_rows > self.sample_size {
((self.total_rows - self.sample_size) as f64 / (self.total_rows - 1) as f64).sqrt()
} else {
1.0
};
(proportion * (1.0 - proportion) / self.sample_size as f64).sqrt() * fpc
}
pub fn confidence_interval(&self, proportion: f64) -> (f64, f64) {
if !self.sampled {
return (proportion, proportion);
}
let z_score = match self.confidence_level {
x if x >= 0.99 => 2.576,
x if x >= 0.95 => 1.96,
x if x >= 0.90 => 1.645,
_ => 1.96, };
let se = self.standard_error(proportion);
let margin = z_score * se;
let lower = (proportion - margin).max(0.0);
let upper = (proportion + margin).min(1.0);
(lower, upper)
}
}
#[cfg(test)]
mod tests {
use rand::{rngs::StdRng, SeedableRng};
use super::*;
#[test]
fn test_sample_size_adaptive() {
let config = SamplingConfig::adaptive();
let (size, should_sample) = config.determine_sample_size(500);
assert_eq!(size, 500);
assert!(!should_sample);
let (size, should_sample) = config.determine_sample_size(50_000);
assert_eq!(size, 5_000);
assert!(should_sample);
let (size, should_sample) = config.determine_sample_size(1_000_000);
assert_eq!(size, 10_000);
assert!(should_sample);
}
#[test]
fn test_sample_size_percentage() {
let config = SamplingConfig::percentage(0.1);
let (size, should_sample) = config.determine_sample_size(1000);
assert_eq!(size, 100);
assert!(should_sample);
let (size, should_sample) = config.determine_sample_size(5);
assert_eq!(size, 1); assert!(should_sample);
}
#[test]
fn test_sample_size_fixed() {
let config = SamplingConfig::fixed(100);
let (size, should_sample) = config.determine_sample_size(1000);
assert_eq!(size, 100);
assert!(should_sample);
let (size, should_sample) = config.determine_sample_size(50);
assert_eq!(size, 50);
assert!(!should_sample);
}
#[test]
fn test_random_sample() {
let mut rng = StdRng::seed_from_u64(42);
let rows: Vec<i32> = (0..100).collect();
let config = SamplingConfig::fixed(10);
let sample = sample_rows(&rows, &config, &mut rng);
assert_eq!(sample.len(), 10);
for val in sample {
assert!((0..100).contains(&val));
}
}
#[test]
fn test_systematic_sample() {
let rows: Vec<i32> = (0..100).collect();
let sample = systematic_sample(&rows, 10);
assert_eq!(sample.len(), 10);
assert_eq!(sample[0], 0);
assert_eq!(sample[1], 10);
assert_eq!(sample[9], 90);
}
#[test]
fn test_reservoir_sample() {
let mut rng = StdRng::seed_from_u64(42);
let rows: Vec<i32> = (0..100).collect();
let sample = reservoir_sample(&rows, 10, &mut rng);
assert_eq!(sample.len(), 10);
for val in sample {
assert!((0..100).contains(&val));
}
}
#[test]
fn test_sample_metadata_extrapolation() {
let metadata = SampleMetadata::new(10_000, 1_000, true, 0.95);
assert_eq!(metadata.extrapolate_count(100), 1_000);
assert_eq!(metadata.extrapolate_count(50), 500);
let freq = metadata.extrapolate_frequency(0.5);
assert!((freq - 0.5).abs() < 0.1);
}
#[test]
fn test_confidence_interval() {
let metadata = SampleMetadata::new(10_000, 1_000, true, 0.95);
let (lower, upper) = metadata.confidence_interval(0.5);
assert!(lower < 0.5);
assert!(upper > 0.5);
assert!((upper - lower) < 0.1); }
#[test]
fn test_no_sampling_for_small_table() {
let mut rng = StdRng::seed_from_u64(42);
let rows: Vec<i32> = (0..50).collect();
let config = SamplingConfig::adaptive();
let sample = sample_rows(&rows, &config, &mut rng);
assert_eq!(sample.len(), 50);
assert_eq!(sample, rows);
}
}