qualia_core_db/solvers/learning/resampling/
mod.rs1pub mod bootstrap;
11pub mod folds;
12pub mod permutation;
13
14pub use bootstrap::{
15 bootstrap_ci, bootstrap_estimate, bootstrap_indices, BootstrapCi, BootstrapResult, CiMethod,
16};
17pub use folds::{k_fold, loocv, train_test_split, Fold};
18pub use permutation::{two_sample_test, PermutationResult};
19
20fn gather_rows(x: &[f64], p: usize, idx: &[usize]) -> Vec<f64> {
23 let mut out = vec![0.0; idx.len() * p];
24 for (r, &i) in idx.iter().enumerate() {
25 out[r * p..(r + 1) * p].copy_from_slice(&x[i * p..(i + 1) * p]);
26 }
27 out
28}
29
30pub fn cross_val_score<F, M>(
37 x: &[f64],
38 y: &[f64],
39 n: usize,
40 p: usize,
41 folds: &[Fold],
42 mut fit_predict: F,
43 metric: M,
44) -> Option<Vec<f64>>
45where
46 F: FnMut(&[f64], &[f64], usize, &[f64], usize) -> Vec<f64>,
47 M: Fn(&[f64], &[f64]) -> Option<f64>,
48{
49 if x.len() != n * p || y.len() != n || folds.is_empty() {
50 return None;
51 }
52 let mut scores = Vec::with_capacity(folds.len());
53 for fold in folds {
54 let n_tr = fold.train.len();
55 let n_te = fold.test.len();
56 if n_tr == 0 || n_te == 0 {
57 return None;
58 }
59 let train_x = gather_rows(x, p, &fold.train);
60 let train_y: Vec<f64> = fold.train.iter().map(|&i| y[i]).collect();
61 let test_x = gather_rows(x, p, &fold.test);
62 let test_y: Vec<f64> = fold.test.iter().map(|&i| y[i]).collect();
63 let preds = fit_predict(&train_x, &train_y, n_tr, &test_x, n_te);
64 if preds.len() != n_te {
65 return None;
66 }
67 scores.push(metric(&test_y, &preds)?);
68 }
69 Some(scores)
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::solvers::learning::metrics::regression::mse;
76 use crate::solvers::learning::regression::linear;
77
78 #[test]
79 fn cross_validates_a_linear_model() {
80 let n = 20;
82 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
83 let y: Vec<f64> = x
84 .iter()
85 .map(|&xi| 2.0 + 3.0 * xi + ((xi as i64 % 3) as f64 - 1.0) * 0.1)
86 .collect();
87 let folds = k_fold(n, 5, true, 1);
88 let scores = cross_val_score(
89 &x,
90 &y,
91 n,
92 1,
93 &folds,
94 |tx, ty, ntr, tex, nte| {
95 let m = linear::fit(tx, ty, ntr, 1, true).unwrap();
96 m.predict(tex, nte, 1)
97 },
98 |yt, yp| mse(yt, yp),
99 )
100 .unwrap();
101 assert_eq!(scores.len(), 5);
102 let mean_mse: f64 = scores.iter().sum::<f64>() / scores.len() as f64;
104 assert!(mean_mse < 0.1, "CV MSE too large: {mean_mse}");
105 }
106
107 #[test]
108 fn loocv_runs_n_folds() {
109 let n = 8;
110 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
111 let y: Vec<f64> = x.iter().map(|&xi| 1.0 + 0.5 * xi).collect();
112 let folds = loocv(n);
113 let scores = cross_val_score(
114 &x,
115 &y,
116 n,
117 1,
118 &folds,
119 |tx, ty, ntr, tex, nte| {
120 linear::fit(tx, ty, ntr, 1, true)
121 .unwrap()
122 .predict(tex, nte, 1)
123 },
124 |yt, yp| mse(yt, yp),
125 )
126 .unwrap();
127 assert_eq!(scores.len(), n);
128 assert!(scores.iter().all(|&s| s < 1e-9));
130 }
131}