use crate::error::Error;
use ahash::AHashMap;
use ndarray::{Array1, Array2, Axis};
use ndarray_rand::rand::seq::SliceRandom;
use std::hash::Hash;
pub type TrainTestSplit<A> = (Array2<f64>, Array2<f64>, Array1<A>, Array1<A>);
pub fn train_test_split<A>(
x: Array2<f64>,
y: Array1<A>,
test_size: Option<f64>,
random_state: Option<u64>,
) -> Result<TrainTestSplit<A>, Error>
where
A: Clone,
{
let n_samples = x.nrows();
if n_samples == 0 {
return Err(Error::empty_input("dataset"));
}
if n_samples != y.len() {
return Err(Error::dimension_mismatch(n_samples, y.len()));
}
let test_size = test_size.unwrap_or(0.3);
if test_size <= 0.0 || test_size >= 1.0 {
return Err(Error::invalid_parameter(
"test_size",
format!(
"test_size must be between 0 and 1 (exclusive), got {}",
test_size
),
));
}
let n_test = if n_samples == 1 {
return Err(Error::invalid_input(
"Cannot split a dataset with only 1 sample into train and test sets",
));
} else if n_samples == 2 {
1 } else {
let calculated = (n_samples as f64 * test_size).round() as usize;
calculated.max(1).min(n_samples - 1)
};
let mut indices: Vec<usize> = (0..n_samples).collect();
let mut rng = crate::random::make_rng(random_state);
indices.shuffle(&mut rng);
let (test_indices, train_indices) = indices.split_at(n_test);
let x_train = x.select(Axis(0), train_indices);
let x_test = x.select(Axis(0), test_indices);
let y_train = y.select(Axis(0), train_indices);
let y_test = y.select(Axis(0), test_indices);
Ok((x_train, x_test, y_train, y_test))
}
pub fn train_test_split_stratified<A>(
x: Array2<f64>,
y: Array1<A>,
test_size: Option<f64>,
random_state: Option<u64>,
) -> Result<TrainTestSplit<A>, Error>
where
A: Clone + Eq + Hash,
{
let n_samples = x.nrows();
if n_samples == 0 {
return Err(Error::empty_input("dataset"));
}
if n_samples != y.len() {
return Err(Error::dimension_mismatch(n_samples, y.len()));
}
let test_size = test_size.unwrap_or(0.3);
if test_size <= 0.0 || test_size >= 1.0 {
return Err(Error::invalid_parameter(
"test_size",
format!(
"test_size must be between 0 and 1 (exclusive), got {}",
test_size
),
));
}
let mut group_of: AHashMap<A, usize> = AHashMap::new();
let mut groups: Vec<Vec<usize>> = Vec::new();
for (idx, label) in y.iter().enumerate() {
match group_of.get(label) {
Some(&g) => groups[g].push(idx),
None => {
group_of.insert(label.clone(), groups.len());
groups.push(vec![idx]);
}
}
}
let mut rng = crate::random::make_rng(random_state);
let mut test_indices: Vec<usize> = Vec::new();
let mut train_indices: Vec<usize> = Vec::new();
for group in groups.iter_mut() {
let class_size = group.len();
if class_size < 2 {
return Err(Error::invalid_input(
"Stratified split requires at least 2 samples per class",
));
}
group.shuffle(&mut rng);
let n_test = ((class_size as f64 * test_size).round() as usize).clamp(1, class_size - 1);
let (test_part, train_part) = group.split_at(n_test);
test_indices.extend_from_slice(test_part);
train_indices.extend_from_slice(train_part);
}
let x_train = x.select(Axis(0), &train_indices);
let x_test = x.select(Axis(0), &test_indices);
let y_train = y.select(Axis(0), &train_indices);
let y_test = y.select(Axis(0), &test_indices);
Ok((x_train, x_test, y_train, y_test))
}