1use prospicio_core::{Error, Result, StreamRng};
8use rayon::prelude::*;
9
10use crate::design::Design;
11use crate::model::{Fitted, Model};
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct Split {
16 pub train: Vec<usize>,
17 pub test: Vec<usize>,
18}
19
20pub fn k_fold(n: usize, k: usize, seed: u64) -> Result<Vec<Split>> {
31 check_k(n, k)?;
32 let order = shuffled(n, seed);
33 Ok(folds(&order, k)
34 .into_iter()
35 .map(|test| complement(n, test))
36 .collect())
37}
38
39pub fn group_k_fold(groups: &[String], k: usize, seed: u64) -> Result<Vec<Split>> {
43 let mut names: Vec<&String> = groups.iter().collect();
44 names.sort();
45 names.dedup();
46 check_k(names.len(), k)?;
47 let order = shuffled(names.len(), seed);
48 let fold_of_group: Vec<usize> = {
49 let mut f = vec![0; names.len()];
50 for (fold, members) in folds(&order, k).into_iter().enumerate() {
51 for g in members {
52 f[g] = fold;
53 }
54 }
55 f
56 };
57 let mut tests = vec![Vec::new(); k];
58 for (row, g) in groups.iter().enumerate() {
59 let gi = names.binary_search(&g).expect("every group is listed");
60 tests[fold_of_group[gi]].push(row);
61 }
62 Ok(tests
63 .into_iter()
64 .map(|test| complement(groups.len(), test))
65 .collect())
66}
67
68pub fn time_ordered(periods: &[i64], n_test: usize) -> Result<Vec<Split>> {
84 let mut distinct: Vec<i64> = periods.to_vec();
85 distinct.sort_unstable();
86 distinct.dedup();
87 if n_test == 0 || n_test >= distinct.len() {
88 return Err(Error::InvalidParameter {
89 name: "n_test",
90 value: n_test as f64,
91 reason: "must be at least 1 and leave an earlier period to train on",
92 });
93 }
94 Ok(distinct[distinct.len() - n_test..]
95 .iter()
96 .map(|&t| Split {
97 train: (0..periods.len()).filter(|&i| periods[i] < t).collect(),
98 test: (0..periods.len()).filter(|&i| periods[i] == t).collect(),
99 })
100 .collect())
101}
102
103pub fn cross_validate<M, S>(
132 model: &M,
133 design: &Design,
134 y: &[f64],
135 splits: &[Split],
136 score: S,
137) -> Result<Vec<f64>>
138where
139 M: Model + Sync,
140 S: Fn(&[f64], &[f64], &Design) -> f64 + Sync,
141{
142 if y.len() != design.n_rows() {
143 return Err(Error::Data(format!(
144 "{} responses for {} design rows",
145 y.len(),
146 design.n_rows()
147 )));
148 }
149 map_splits(splits, |s| {
150 let train = design.select(&s.train);
151 let y_train: Vec<f64> = s.train.iter().map(|&i| y[i]).collect();
152 let fitted = model.fit(&train, &y_train)?;
153 let test = design.select(&s.test);
154 let y_test: Vec<f64> = s.test.iter().map(|&i| y[i]).collect();
155 let pred = fitted.predict(&test)?;
156 Ok(score(&y_test, &pred, &test))
157 })
158}
159
160pub fn map_splits<T, F>(splits: &[Split], f: F) -> Result<Vec<T>>
166where
167 T: Send,
168 F: Fn(&Split) -> Result<T> + Sync + Send,
169{
170 let results: Vec<Result<T>> = splits.par_iter().map(&f).collect();
171 results.into_iter().collect()
172}
173
174#[derive(Debug, Clone, PartialEq)]
177pub struct GridSearch<P> {
178 pub scores: Vec<(P, f64)>,
180 pub best: usize,
182}
183
184pub fn grid_search<P, M, B, S>(
188 candidates: Vec<P>,
189 make: B,
190 design: &Design,
191 y: &[f64],
192 splits: &[Split],
193 score: S,
194) -> Result<GridSearch<P>>
195where
196 M: Model + Sync,
197 B: Fn(&P) -> M,
198 S: Fn(&[f64], &[f64], &Design) -> f64 + Sync,
199{
200 if candidates.is_empty() {
201 return Err(Error::InvalidParameter {
202 name: "candidates",
203 value: 0.0,
204 reason: "must not be empty",
205 });
206 }
207 let mut scores = Vec::with_capacity(candidates.len());
208 for c in candidates {
209 let s = cross_validate(&make(&c), design, y, splits, &score)?;
210 let mean = s.iter().sum::<f64>() / s.len() as f64;
211 scores.push((c, mean));
212 }
213 let best = scores
214 .iter()
215 .enumerate()
216 .min_by(|a, b| a.1.1.total_cmp(&b.1.1))
217 .map(|(i, _)| i)
218 .expect("not empty");
219 Ok(GridSearch { scores, best })
220}
221
222#[allow(clippy::too_many_arguments)]
266pub fn random_search<P, M, D, B, S>(
267 n: usize,
268 seed: u64,
269 mut draw: D,
270 make: B,
271 design: &Design,
272 y: &[f64],
273 splits: &[Split],
274 score: S,
275) -> Result<GridSearch<P>>
276where
277 M: Model + Sync,
278 D: FnMut(&mut StreamRng) -> P,
279 B: Fn(&P) -> M,
280 S: Fn(&[f64], &[f64], &Design) -> f64 + Sync,
281{
282 let mut rng = StreamRng::new(seed, 0);
283 let candidates = (0..n).map(|_| draw(&mut rng)).collect();
284 grid_search(candidates, make, design, y, splits, score)
285}
286
287pub fn log_uniform(rng: &mut StreamRng, low: f64, high: f64) -> f64 {
290 (low.ln() + rng.next_open01() * (high.ln() - low.ln())).exp()
291}
292
293pub fn uniform_int(rng: &mut StreamRng, low: usize, high: usize) -> usize {
295 let span = (high - low + 1) as f64;
296 low + ((rng.next_open01() * span) as usize).min(high - low)
297}
298
299fn check_k(n: usize, k: usize) -> Result<()> {
300 if k < 2 || k > n {
301 return Err(Error::InvalidParameter {
302 name: "k",
303 value: k as f64,
304 reason: "must be at least 2 and at most the number of rows (or groups)",
305 });
306 }
307 Ok(())
308}
309
310pub fn permutation(n: usize, seed: u64, stream: u64) -> Vec<usize> {
323 let mut order: Vec<usize> = (0..n).collect();
324 let mut rng = StreamRng::new(seed, stream);
325 for i in (1..n).rev() {
326 let j = ((rng.next_open01() * (i + 1) as f64) as usize).min(i);
327 order.swap(i, j);
328 }
329 order
330}
331
332fn shuffled(n: usize, seed: u64) -> Vec<usize> {
334 permutation(n, seed, 0)
335}
336
337fn folds(order: &[usize], k: usize) -> Vec<Vec<usize>> {
339 let n = order.len();
340 let (base, extra) = (n / k, n % k);
341 let mut out = Vec::with_capacity(k);
342 let mut start = 0;
343 for i in 0..k {
344 let len = base + usize::from(i < extra);
345 let mut fold = order[start..start + len].to_vec();
346 fold.sort_unstable();
347 out.push(fold);
348 start += len;
349 }
350 out
351}
352
353fn complement(n: usize, test: Vec<usize>) -> Split {
354 let mut in_test = vec![false; n];
355 for &i in &test {
356 in_test[i] = true;
357 }
358 Split {
359 train: (0..n).filter(|&i| !in_test[i]).collect(),
360 test,
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn map_splits_keeps_order_and_the_first_error() {
370 let splits = k_fold(40, 8, 3).unwrap();
371 let sums = map_splits(&splits, |s| Ok(s.test.iter().sum::<usize>())).unwrap();
372 let serial: Vec<usize> = splits.iter().map(|s| s.test.iter().sum()).collect();
373 assert_eq!(sums, serial);
374 let err = map_splits(&splits, |s| {
375 if s.test.contains(&splits[5].test[0]) || s.test.contains(&splits[2].test[0]) {
376 Err(Error::Data(format!("fold with {}", s.test[0])))
377 } else {
378 Ok(())
379 }
380 })
381 .unwrap_err();
382 assert_eq!(
383 err.to_string(),
384 Error::Data(format!("fold with {}", splits[2].test[0])).to_string()
385 );
386 }
387
388 #[test]
389 fn k_fold_partitions_and_replays() {
390 let s = k_fold(23, 4, 9).unwrap();
391 let mut all: Vec<usize> = s.iter().flat_map(|x| x.test.clone()).collect();
392 all.sort_unstable();
393 assert_eq!(all, (0..23).collect::<Vec<_>>());
394 assert_eq!(s, k_fold(23, 4, 9).unwrap());
395 assert_ne!(s, k_fold(23, 4, 10).unwrap());
396 assert!(k_fold(3, 4, 1).is_err());
397 }
398
399 #[test]
400 fn permutations_replay_and_differ_by_stream() {
401 let p = permutation(50, 4, 2);
402 assert_eq!(p, permutation(50, 4, 2));
403 assert_ne!(p, permutation(50, 4, 3));
404 assert_eq!(permutation(50, 4, 0), shuffled(50, 4));
405 let mut sorted = p.clone();
406 sorted.sort_unstable();
407 assert_eq!(sorted, (0..50).collect::<Vec<_>>());
408 assert!(permutation(0, 1, 0).is_empty());
409 }
410
411 #[test]
412 fn groups_stay_together() {
413 let groups: Vec<String> = ["a", "b", "a", "c", "b", "d", "c"]
414 .iter()
415 .map(|s| s.to_string())
416 .collect();
417 for s in group_k_fold(&groups, 2, 3).unwrap() {
418 for &i in &s.test {
419 assert!(s.train.iter().all(|&j| groups[j] != groups[i]));
420 }
421 }
422 }
423}