model_selection_rs/splitters/
shuffle_split.rs1use rand::rngs::StdRng;
4use rand::seq::SliceRandom;
5use rand::SeedableRng;
6
7use super::CvSplitter;
8use crate::error::{ModelSelectionError, Result};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum SubsetSize {
14 Count(usize),
16 Fraction(f64),
18}
19
20impl SubsetSize {
21 pub(crate) fn resolve(self, n_samples: usize) -> usize {
23 match self {
24 SubsetSize::Count(c) => c,
25 SubsetSize::Fraction(f) => (f * n_samples as f64).round() as usize,
26 }
27 }
28}
29
30#[derive(Debug, Clone)]
50pub struct ShuffleSplit {
51 n_splits: usize,
52 test_size: SubsetSize,
53 train_size: Option<SubsetSize>,
54 seed: u64,
55}
56
57impl ShuffleSplit {
58 #[must_use]
61 pub fn new(n_splits: usize) -> Self {
62 Self {
63 n_splits,
64 test_size: SubsetSize::Fraction(0.1),
65 train_size: None,
66 seed: 0,
67 }
68 }
69
70 #[must_use]
72 pub fn with_test_size(mut self, test_size: SubsetSize) -> Self {
73 self.test_size = test_size;
74 self
75 }
76
77 #[must_use]
79 pub fn with_train_size(mut self, train_size: SubsetSize) -> Self {
80 self.train_size = Some(train_size);
81 self
82 }
83
84 #[must_use]
86 pub fn with_seed(mut self, seed: u64) -> Self {
87 self.seed = seed;
88 self
89 }
90
91 pub(crate) fn resolve_sizes(&self, n_samples: usize) -> Result<(usize, usize)> {
94 let n_test = self.test_size.resolve(n_samples);
95 let n_train = match self.train_size {
96 Some(ts) => ts.resolve(n_samples),
97 None => n_samples.saturating_sub(n_test),
98 };
99 if n_test == 0 || n_train == 0 {
100 return Err(ModelSelectionError::InvalidSplitCount {
101 msg: format!(
102 "resolved train={n_train}, test={n_test}; both must be >= 1 \
103 (n_samples={n_samples})"
104 ),
105 });
106 }
107 if n_train + n_test > n_samples {
108 return Err(ModelSelectionError::NotEnoughSamples {
109 needed: n_train + n_test,
110 got: n_samples,
111 });
112 }
113 Ok((n_train, n_test))
114 }
115}
116
117impl CvSplitter for ShuffleSplit {
118 fn split(&self, n_samples: usize) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
119 let (n_train, n_test) = self.resolve_sizes(n_samples)?;
120 let mut splits = Vec::with_capacity(self.n_splits);
121 for i in 0..self.n_splits {
122 let mut rng = StdRng::seed_from_u64(self.seed.wrapping_add(i as u64));
124 let mut indices: Vec<usize> = (0..n_samples).collect();
125 indices.shuffle(&mut rng);
126 let test: Vec<usize> = indices[..n_test].to_vec();
127 let train: Vec<usize> = indices[n_test..n_test + n_train].to_vec();
128 splits.push((train, test));
129 }
130 Ok(splits)
131 }
132
133 fn n_splits(&self) -> usize {
134 self.n_splits
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use std::collections::HashSet;
142
143 #[test]
144 fn sizes_honoured_and_disjoint() {
145 let ss = ShuffleSplit::new(4)
146 .with_test_size(SubsetSize::Count(5))
147 .with_train_size(SubsetSize::Count(10))
148 .with_seed(3);
149 for (train, test) in ss.split(30).unwrap() {
150 assert_eq!(train.len(), 10);
151 assert_eq!(test.len(), 5);
152 let tr: HashSet<_> = train.iter().collect();
153 let te: HashSet<_> = test.iter().collect();
154 assert!(tr.is_disjoint(&te));
155 }
156 }
157
158 #[test]
159 fn fraction_test_size() {
160 let ss = ShuffleSplit::new(2).with_test_size(SubsetSize::Fraction(0.2));
161 let splits = ss.split(50).unwrap();
162 assert!(splits.iter().all(|(_, te)| te.len() == 10));
163 }
164
165 #[test]
166 fn deterministic_for_seed() {
167 let a = ShuffleSplit::new(3).with_seed(11).split(20).unwrap();
168 let b = ShuffleSplit::new(3).with_seed(11).split(20).unwrap();
169 assert_eq!(a, b);
170 }
171
172 #[test]
173 fn errors_when_sizes_dont_fit() {
174 let ss = ShuffleSplit::new(2)
175 .with_test_size(SubsetSize::Count(20))
176 .with_train_size(SubsetSize::Count(20));
177 assert!(matches!(
178 ss.split(30),
179 Err(ModelSelectionError::NotEnoughSamples { .. })
180 ));
181 }
182}