use crate::dataframe::DataFrame;
use crate::error::{Error, Result};
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::Rng;
use scirs2_core::random::RngExt;
use scirs2_core::random::SeedableRng;
use scirs2_core::random::SliceRandom;
use std::collections::HashMap;
fn seeded_rng(seed: Option<u64>) -> StdRng {
match seed {
Some(seed_val) => StdRng::seed_from_u64(seed_val),
None => {
let mut seed_bytes = [0u8; 32];
scirs2_core::random::rng().fill_bytes(&mut seed_bytes);
StdRng::from_seed(seed_bytes)
}
}
}
pub(crate) fn sample_impl(
df: &DataFrame,
fraction: f64,
replace: bool,
seed: Option<u64>,
) -> Result<DataFrame> {
if fraction <= 0.0 {
return Err(Error::InvalidValue(
"Sample rate must be a positive value".into(),
));
}
let n_rows = df.row_count();
if n_rows == 0 {
return Ok(DataFrame::new());
}
let sample_size = (n_rows as f64 * fraction).ceil() as usize;
if !replace && sample_size > n_rows {
return Err(Error::InvalidOperation(
"For sampling without replacement, sample size must not exceed original data size"
.into(),
));
}
let mut rng = seeded_rng(seed);
let indices = if replace {
(0..sample_size)
.map(|_| rng.random_range(0..n_rows))
.collect::<Vec<_>>()
} else {
let mut idx: Vec<usize> = (0..n_rows).collect();
idx.shuffle(&mut rng);
idx[0..sample_size].to_vec()
};
df.sample(&indices)
}
pub(crate) fn bootstrap_impl(data: &[f64], n_samples: usize) -> Result<Vec<Vec<f64>>> {
if data.is_empty() {
return Err(Error::EmptyData("Bootstrap requires data".into()));
}
if n_samples == 0 {
return Err(Error::InvalidValue(
"Number of samples must be positive".into(),
));
}
let n = data.len();
let mut rng = scirs2_core::random::rng();
let mut result = Vec::with_capacity(n_samples);
for _ in 0..n_samples {
let sample: Vec<f64> = (0..n).map(|_| data[rng.random_range(0..n)]).collect();
result.push(sample);
}
Ok(result)
}
pub fn stratified_sample_impl(
df: &DataFrame,
strata_column: &str,
fraction: f64,
replace: bool,
seed: Option<u64>,
) -> Result<DataFrame> {
if !df.contains_column(strata_column) {
return Err(Error::ColumnNotFound(strata_column.to_string()));
}
if fraction <= 0.0 {
return Err(Error::InvalidValue(
"Sample rate must be a positive value".into(),
));
}
let strata_col = match df.get_column::<String>(strata_column) {
Ok(col) => col,
Err(_) => return Err(Error::ColumnNotFound(strata_column.to_string())),
};
let mut strata_indices: HashMap<String, Vec<usize>> = HashMap::new();
for (i, value) in strata_col.values().iter().enumerate() {
strata_indices.entry(value.clone()).or_default().push(i);
}
let mut sorted_strata: Vec<&String> = strata_indices.keys().collect();
sorted_strata.sort();
let mut rng = seeded_rng(seed);
let mut all_sample_indices = Vec::new();
for stratum in sorted_strata {
let indices = &strata_indices[stratum];
let sample_size = (indices.len() as f64 * fraction).ceil() as usize;
if sample_size == 0 {
continue;
}
if replace {
for _ in 0..sample_size {
let idx = indices[rng.random_range(0..indices.len())];
all_sample_indices.push(idx);
}
} else {
if sample_size > indices.len() {
return Err(Error::InvalidOperation(
"For sampling without replacement, sample size must not exceed stratum size"
.into(),
));
}
let mut sampled_indices = indices.clone();
sampled_indices.shuffle(&mut rng);
all_sample_indices.extend_from_slice(&sampled_indices[0..sample_size]);
}
}
all_sample_indices.sort();
df.sample(&all_sample_indices)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dataframe::DataFrame;
use crate::series::Series;
#[test]
fn test_simple_sample() {
let mut df = DataFrame::new();
let data = Series::new(
vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
Some("data".to_string()),
)
.expect("operation should succeed");
df.add_column("data".to_string(), data)
.expect("operation should succeed");
let sample = sample_impl(&df, 0.5, false, None).expect("operation should succeed");
assert_eq!(sample.column_count(), df.column_count());
assert_eq!(sample.row_count(), 5);
let sampled_values = sample
.get_column::<i32>("data")
.expect("the i32 column must round-trip through sampling")
.values()
.to_vec();
assert_eq!(sampled_values.len(), 5);
for v in &sampled_values {
assert!((1..=10).contains(v));
}
let sample = sample_impl(&df, 0.3, true, None).expect("operation should succeed");
assert_eq!(sample.column_count(), df.column_count());
let sample = sample_impl(&df, 2.0, true, None).expect("operation should succeed");
assert_eq!(sample.column_count(), df.column_count());
let result = sample_impl(&df, 2.0, false, None);
assert!(result.is_err());
}
#[test]
fn test_sample_is_reproducible_with_seed() {
let mut df = DataFrame::new();
let data = Series::new((0..50).collect::<Vec<i64>>(), Some("data".to_string()))
.expect("operation should succeed");
df.add_column("data".to_string(), data)
.expect("operation should succeed");
let a = sample_impl(&df, 0.5, false, Some(42))
.expect("operation should succeed")
.get_column::<i64>("data")
.expect("column present")
.values()
.to_vec();
let b = sample_impl(&df, 0.5, false, Some(42))
.expect("operation should succeed")
.get_column::<i64>("data")
.expect("column present")
.values()
.to_vec();
assert_eq!(a, b, "the same seed must draw byte-identical samples");
let c = sample_impl(&df, 0.5, false, Some(43))
.expect("operation should succeed")
.get_column::<i64>("data")
.expect("column present")
.values()
.to_vec();
assert_ne!(
a, c,
"a different seed should (overwhelmingly likely) draw a different sample"
);
}
#[test]
fn test_bootstrap() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let bootstrap_samples = bootstrap_impl(&data, 10).expect("operation should succeed");
assert_eq!(bootstrap_samples.len(), 10);
for sample in &bootstrap_samples {
assert_eq!(sample.len(), data.len());
}
for sample in &bootstrap_samples {
for value in sample {
assert!(data.contains(value));
}
}
}
fn stratified_test_df() -> DataFrame {
let mut df = DataFrame::new();
let strata = Series::new(
vec![
"a".to_string(),
"a".to_string(),
"a".to_string(),
"a".to_string(),
"b".to_string(),
"b".to_string(),
"b".to_string(),
"b".to_string(),
],
Some("stratum".to_string()),
)
.expect("operation should succeed");
let value = Series::new(vec![1i64, 2, 3, 4, 5, 6, 7, 8], Some("value".to_string()))
.expect("operation should succeed");
df.add_column("stratum".to_string(), strata)
.expect("operation should succeed");
df.add_column("value".to_string(), value)
.expect("operation should succeed");
df
}
#[test]
fn test_stratified_sample_preserves_non_string_columns() {
let df = stratified_test_df();
let sample = stratified_sample_impl(&df, "stratum", 0.5, false, None)
.expect("operation should succeed");
assert_eq!(sample.column_count(), df.column_count());
let values = sample
.get_column::<i64>("value")
.expect("the i64 column must round-trip through stratified sampling")
.values()
.to_vec();
assert_eq!(values.len(), 4);
}
#[test]
fn test_stratified_sample_is_reproducible_with_seed() {
let df = stratified_test_df();
let a = stratified_sample_impl(&df, "stratum", 0.5, false, Some(7))
.expect("operation should succeed")
.get_column::<i64>("value")
.expect("column present")
.values()
.to_vec();
for _ in 0..20 {
let b = stratified_sample_impl(&df, "stratum", 0.5, false, Some(7))
.expect("operation should succeed")
.get_column::<i64>("value")
.expect("column present")
.values()
.to_vec();
assert_eq!(
a, b,
"the same seed must draw a byte-identical stratified sample every time"
);
}
}
}