use crate::error::{DatarustError, Result};
use crate::matrix::Matrix;
use crate::model_selection::rng::Rng;
#[derive(Debug, Clone)]
pub struct TrainTestSplit {
test_size: f64,
shuffle: bool,
random_state: Option<u64>,
}
impl Default for TrainTestSplit {
fn default() -> Self {
Self::new()
}
}
impl TrainTestSplit {
pub fn new() -> Self {
Self {
test_size: 0.25,
shuffle: true,
random_state: None,
}
}
pub fn with_test_size(mut self, ratio: f64) -> Self {
self.test_size = ratio;
self
}
pub fn with_shuffle(mut self, shuffle: bool) -> Self {
self.shuffle = shuffle;
self
}
pub fn with_random_state(mut self, seed: u64) -> Self {
self.random_state = Some(seed);
self
}
pub fn split(&self, x: &Matrix, y: &[f64]) -> Result<(Matrix, Matrix, Vec<f64>, Vec<f64>)> {
let n = x.nrows();
if n == 0 {
return Err(DatarustError::EmptyInput("X has no rows".into()));
}
if y.len() != n {
return Err(DatarustError::ShapeMismatch {
expected: format!("{} targets", n),
actual: format!("{} targets", y.len()),
});
}
if self.test_size <= 0.0 || self.test_size >= 1.0 {
return Err(DatarustError::InvalidInput(format!(
"test_size must be in (0, 1), got {}",
self.test_size
)));
}
let mut indices: Vec<usize> = (0..n).collect();
if self.shuffle {
let seed = self.random_state.unwrap_or(0x9E3779B97F4A7C15);
Rng::new(seed).shuffle(&mut indices);
}
let n_test = (n as f64 * self.test_size).round() as usize;
let n_test = n_test.clamp(1, n - 1);
let (test_idx, train_idx) = partition_indices(&indices, n_test);
let x_test = x.select_rows(&test_idx)?;
let x_train = x.select_rows(&train_idx)?;
let y_test: Vec<f64> = test_idx.iter().map(|&i| y[i]).collect();
let y_train: Vec<f64> = train_idx.iter().map(|&i| y[i]).collect();
Ok((x_train, x_test, y_train, y_test))
}
}
pub fn train_test_split(x: &Matrix, y: &[f64]) -> Result<(Matrix, Matrix, Vec<f64>, Vec<f64>)> {
TrainTestSplit::new().split(x, y)
}
fn partition_indices(indices: &[usize], n_test: usize) -> (Vec<usize>, Vec<usize>) {
let test = indices[..n_test].to_vec();
let train = indices[n_test..].to_vec();
(test, train)
}
#[cfg(test)]
mod tests {
use super::*;
fn make(n: usize) -> (Matrix, Vec<f64>) {
let rows: Vec<Vec<f64>> = (0..n).map(|i| vec![i as f64]).collect();
let y: Vec<f64> = (0..n).map(|i| i as f64).collect();
(Matrix::new(rows).unwrap(), y)
}
#[test]
fn default_split_sizes() {
let (x, y) = make(100);
let (x_tr, x_te, y_tr, y_te) = train_test_split(&x, &y).unwrap();
assert_eq!(x_tr.nrows(), 75);
assert_eq!(x_te.nrows(), 25);
assert_eq!(y_tr.len(), 75);
assert_eq!(y_te.len(), 25);
}
#[test]
fn custom_test_size() {
let (x, y) = make(100);
let (x_tr, x_te, _, _) = TrainTestSplit::new()
.with_test_size(0.3)
.split(&x, &y)
.unwrap();
assert_eq!(x_te.nrows(), 30);
assert_eq!(x_tr.nrows(), 70);
}
#[test]
fn no_shuffle_preserves_order() {
let (x, y) = make(10);
let (_, x_te, _, y_te) = TrainTestSplit::new()
.with_shuffle(false)
.split(&x, &y)
.unwrap();
assert_eq!(x_te.get(0, 0), 0.0);
assert_eq!(x_te.get(2, 0), 2.0);
assert_eq!(y_te[0], 0.0);
}
#[test]
fn shuffle_deterministic_with_seed() {
let (x, y) = make(100);
let s = TrainTestSplit::new()
.with_test_size(0.2)
.with_random_state(42);
let (_, x_te_a, _, _) = s.clone().split(&x, &y).unwrap();
let (_, x_te_b, _, _) = s.split(&x, &y).unwrap();
for i in 0..x_te_a.nrows() {
assert_eq!(x_te_a.get(i, 0), x_te_b.get(i, 0));
}
}
#[test]
fn all_samples_present_after_split() {
let (x, y) = make(50);
let (x_tr, x_te, y_tr, y_te) = train_test_split(&x, &y).unwrap();
let mut all: Vec<f64> = (0..x_tr.nrows()).map(|i| x_tr.get(i, 0)).collect();
all.extend((0..x_te.nrows()).map(|i| x_te.get(i, 0)));
all.sort_by(|a, b| a.partial_cmp(b).unwrap());
for (i, v) in all.iter().enumerate() {
assert_eq!(*v, i as f64);
}
let _ = (y_tr, y_te);
}
#[test]
fn empty_input_rejected() {
let x = Matrix::new(vec![vec![1.0]]).unwrap();
let err = TrainTestSplit::new().split(&x, &[]).unwrap_err();
assert!(matches!(err, DatarustError::ShapeMismatch { .. }));
}
#[test]
fn invalid_test_size_rejected() {
let (x, y) = make(10);
let err = TrainTestSplit::new()
.with_test_size(0.0)
.split(&x, &y)
.unwrap_err();
assert!(matches!(err, DatarustError::InvalidInput(_)));
let err = TrainTestSplit::new()
.with_test_size(1.0)
.split(&x, &y)
.unwrap_err();
assert!(matches!(err, DatarustError::InvalidInput(_)));
}
#[test]
fn y_length_mismatch_rejected() {
let (x, _) = make(10);
let err = TrainTestSplit::new().split(&x, &[0.0, 1.0]).unwrap_err();
assert!(matches!(err, DatarustError::ShapeMismatch { .. }));
}
#[test]
fn small_dataset_clamps_test_size() {
let (x, y) = make(3);
let (_, x_te, _, _) = train_test_split(&x, &y).unwrap();
assert_eq!(x_te.nrows(), 1);
}
}