1use crate::linear::common::{
7 core_array1_to_py, core_array2_to_py, pyarray_to_core_array1, pyarray_to_core_array2,
8 PyValueError,
9};
10use numpy::{PyArray1, PyArray2, PyReadonlyArray1, PyReadonlyArray2, PyUntypedArrayMethods};
11use pyo3::prelude::*;
12use scirs2_core::ndarray::{Array1, Array2};
13use sklears_model_selection::{train_test_split as core_train_test_split, CrossValidator, KFold};
14
15type TrainTestSplitResult = (
17 Py<PyArray2<f64>>,
18 Py<PyArray2<f64>>,
19 Py<PyArray1<f64>>,
20 Py<PyArray1<f64>>,
21);
22
23type CoreTrainTestSplitResult = (Array2<f64>, Array2<f64>, Array1<f64>, Array1<f64>);
26
27pub fn train_test_split_core(
47 x: &Array2<f64>,
48 y: &Array1<f64>,
49 test_size: Option<f64>,
50 random_state: Option<u64>,
51) -> PyResult<CoreTrainTestSplitResult> {
52 core_train_test_split(x, y, test_size.unwrap_or(0.25), random_state)
53 .map_err(|e| PyValueError::new_err(format!("train_test_split failed: {e}")))
54}
55
56#[pyfunction]
64#[pyo3(signature = (x, y, test_size=None, random_state=None))]
65pub fn train_test_split(
66 py: Python<'_>,
67 x: PyReadonlyArray2<f64>,
68 y: PyReadonlyArray1<f64>,
69 test_size: Option<f64>,
70 random_state: Option<u64>,
71) -> PyResult<TrainTestSplitResult> {
72 let x_arr = pyarray_to_core_array2(x)?;
73 let y_arr = pyarray_to_core_array1(y)?;
74
75 let (x_train, x_test, y_train, y_test) =
76 train_test_split_core(&x_arr, &y_arr, test_size, random_state)?;
77
78 Ok((
79 core_array2_to_py(py, &x_train)?,
80 core_array2_to_py(py, &x_test)?,
81 core_array1_to_py(py, &y_train),
82 core_array1_to_py(py, &y_test),
83 ))
84}
85
86#[pyclass(name = "KFold")]
92pub struct PyKFold {
93 inner: KFold,
94}
95
96impl PyKFold {
97 pub fn split_core(&self, n_samples: usize) -> PyResult<Vec<(Vec<usize>, Vec<usize>)>> {
102 let n_splits = self.inner.n_splits();
103 if n_splits > n_samples {
104 return Err(PyValueError::new_err(format!(
105 "Cannot have number of splits n_splits={n_splits} greater than the \
106 number of samples n_samples={n_samples}"
107 )));
108 }
109 Ok(self.inner.split(n_samples, None))
110 }
111}
112
113#[pymethods]
114impl PyKFold {
115 #[new]
120 #[pyo3(signature = (n_splits=5, shuffle=false, random_state=None))]
121 pub fn new(n_splits: usize, shuffle: bool, random_state: Option<u64>) -> PyResult<Self> {
122 if n_splits < 2 {
126 return Err(PyValueError::new_err(format!(
127 "n_splits must be at least 2, got {n_splits}"
128 )));
129 }
130
131 let mut inner = KFold::new(n_splits).shuffle(shuffle);
132 if let Some(seed) = random_state {
133 inner = inner.random_state(seed);
134 }
135
136 Ok(Self { inner })
137 }
138
139 fn get_n_splits(&self) -> usize {
140 self.inner.n_splits()
141 }
142
143 #[pyo3(signature = (x, y=None))]
153 fn split(
154 &self,
155 x: PyReadonlyArray2<f64>,
156 y: Option<Bound<'_, PyAny>>,
157 ) -> PyResult<Vec<(Vec<usize>, Vec<usize>)>> {
158 let _ = y;
159 let n_samples = x.shape()[0];
160 self.split_core(n_samples)
161 }
162
163 fn __repr__(&self) -> String {
164 format!("KFold(n_splits={})", self.inner.n_splits())
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn make_indexed_dataset(n_samples: usize, n_features: usize) -> (Array2<f64>, Array1<f64>) {
173 let x_data: Vec<f64> = (0..n_samples * n_features).map(|v| v as f64).collect();
174 let x = Array2::from_shape_vec((n_samples, n_features), x_data)
175 .expect("shape matches data length");
176 let y = Array1::from_vec((0..n_samples).map(|i| i as f64).collect());
177 (x, y)
178 }
179
180 #[test]
181 fn train_test_split_respects_requested_test_size() {
182 let (x, y) = make_indexed_dataset(100, 2);
183
184 let (x_train, x_test, y_train, y_test) =
185 train_test_split_core(&x, &y, Some(0.3), Some(7)).expect("split should succeed");
186
187 assert_eq!(x_test.nrows(), 30);
188 assert_eq!(x_train.nrows(), 70);
189 assert_eq!(y_test.len(), 30);
190 assert_eq!(y_train.len(), 70);
191 }
192
193 #[test]
194 fn train_test_split_train_and_test_are_disjoint_and_cover_everything() {
195 let (x, y) = make_indexed_dataset(100, 2);
196
197 let (_, _, y_train, y_test) =
198 train_test_split_core(&x, &y, Some(0.3), Some(7)).expect("split should succeed");
199
200 let mut seen: Vec<usize> = y_train
201 .iter()
202 .chain(y_test.iter())
203 .map(|&v| v as usize)
204 .collect();
205 seen.sort_unstable();
206
207 assert_eq!(seen, (0..100).collect::<Vec<_>>());
211 }
212
213 #[test]
214 fn train_test_split_same_random_state_is_deterministic() {
215 let (x, y) = make_indexed_dataset(50, 3);
216
217 let (_, _, y_train1, y_test1) =
218 train_test_split_core(&x, &y, Some(0.25), Some(123)).expect("split should succeed");
219 let (_, _, y_train2, y_test2) =
220 train_test_split_core(&x, &y, Some(0.25), Some(123)).expect("split should succeed");
221
222 assert_eq!(y_train1, y_train2);
223 assert_eq!(y_test1, y_test2);
224 }
225
226 #[test]
227 fn train_test_split_defaults_test_size_to_a_quarter() {
228 let (x, y) = make_indexed_dataset(40, 2);
229
230 let (_, x_test, _, _) =
231 train_test_split_core(&x, &y, None, Some(1)).expect("split should succeed");
232
233 assert_eq!(x_test.nrows(), 10);
234 }
235
236 #[test]
237 fn kfold_produces_n_splits_folds_covering_every_index_exactly_once() {
238 let kfold = PyKFold::new(5, false, None).expect("n_splits=5 is valid");
239 let folds = kfold.split_core(100).expect("split should succeed");
240
241 assert_eq!(folds.len(), 5);
242
243 let mut all_test_indices: Vec<usize> = folds
244 .iter()
245 .flat_map(|(_, test)| test.iter().copied())
246 .collect();
247 all_test_indices.sort_unstable();
248 assert_eq!(all_test_indices, (0..100).collect::<Vec<_>>());
249
250 for (train, test) in &folds {
252 assert_eq!(train.len() + test.len(), 100);
253 }
254 }
255
256 #[test]
257 fn kfold_new_rejects_n_splits_below_two_with_value_error_not_panic() {
258 assert!(PyKFold::new(1, false, None).is_err());
259 assert!(PyKFold::new(0, false, None).is_err());
260 }
261}