zsfm_tabfm/ensemble/
oof.rs1use anyhow::Result;
7use serde_json::Value;
8
9use crate::infer::BurnTabFMModel;
10
11use super::config_gen::MemberConfig;
12use super::orchestrate::{run_members_classification, run_members_regression, EnsembleParams};
13use super::pyrandom::PyRandom;
14
15pub struct KFoldSplit {
16 pub train_idx: Vec<usize>,
17 pub val_idx: Vec<usize>,
18}
19
20pub fn kfold_splits(n: usize, k: usize, random_state: u64) -> Vec<KFoldSplit> {
22 let mut indices: Vec<usize> = (0..n).collect();
23 PyRandom::new(random_state).shuffle(&mut indices);
24
25 let k = k.min(n).max(1);
26 let base = n / k;
27 let remainder = n % k;
28 let mut folds = Vec::with_capacity(k);
29 let mut start = 0;
30 for i in 0..k {
31 let size = base + if i < remainder { 1 } else { 0 };
32 let val_idx: Vec<usize> = indices[start..start + size].to_vec();
33 let train_idx: Vec<usize> = indices
34 .iter()
35 .enumerate()
36 .filter(|(pos, _)| *pos < start || *pos >= start + size)
37 .map(|(_, &v)| v)
38 .collect();
39 folds.push(KFoldSplit { train_idx, val_idx });
40 start += size;
41 }
42 folds
43}
44
45fn select_rows<T: Clone>(rows: &[T], idx: &[usize]) -> Vec<T> {
46 idx.iter().map(|&i| rows[i].clone()).collect()
47}
48
49pub fn run_oof_classification(
63 model: &BurnTabFMModel,
64 x_train_raw: &[Vec<Value>],
65 y_train_codes: &[f64],
66 cat_mask: &[bool],
67 n_classes: usize,
68 configs: &[MemberConfig],
69 p: &EnsembleParams,
70 num_folds: usize,
71) -> Result<Vec<Vec<Vec<f64>>>> {
72 let n = x_train_raw.len();
73 let folds = kfold_splits(n, num_folds, p.random_state);
74
75 let mut oof: Vec<Vec<Vec<f64>>> = vec![vec![Vec::new(); n]; configs.len()];
76 for fold in &folds {
77 let fold_x_train = select_rows(x_train_raw, &fold.train_idx);
78 let fold_y_train: Vec<f64> = fold.train_idx.iter().map(|&i| y_train_codes[i]).collect();
79 let fold_x_val = select_rows(x_train_raw, &fold.val_idx);
80
81 let per_member = run_members_classification(
82 model,
83 &fold_x_train,
84 &fold_y_train,
85 &fold_x_val,
86 cat_mask,
87 n_classes,
88 configs,
89 p.outlier_threshold,
90 p.batch_size,
91 )?;
92 for (m, member_out) in per_member.into_iter().enumerate() {
93 for (local_idx, &orig_idx) in fold.val_idx.iter().enumerate() {
94 oof[m][orig_idx] = member_out[local_idx].clone();
95 }
96 }
97 }
98 Ok(oof)
99}
100
101pub fn run_oof_regression(
103 model: &BurnTabFMModel,
104 x_train_raw: &[Vec<Value>],
105 y_train_scaled: &[f64],
106 cat_mask: &[bool],
107 configs: &[MemberConfig],
108 p: &EnsembleParams,
109 num_folds: usize,
110) -> Result<Vec<Vec<f64>>> {
111 let n = x_train_raw.len();
112 let folds = kfold_splits(n, num_folds, p.random_state);
113
114 let mut oof: Vec<Vec<f64>> = vec![vec![0.0; n]; configs.len()];
115 for fold in &folds {
116 let fold_x_train = select_rows(x_train_raw, &fold.train_idx);
117 let fold_y_train: Vec<f64> = fold.train_idx.iter().map(|&i| y_train_scaled[i]).collect();
118 let fold_x_val = select_rows(x_train_raw, &fold.val_idx);
119
120 let per_member = run_members_regression(
121 model,
122 &fold_x_train,
123 &fold_y_train,
124 &fold_x_val,
125 cat_mask,
126 configs,
127 p.outlier_threshold,
128 p.batch_size,
129 )?;
130 for (m, member_out) in per_member.into_iter().enumerate() {
131 for (local_idx, &orig_idx) in fold.val_idx.iter().enumerate() {
132 oof[m][orig_idx] = member_out[local_idx];
133 }
134 }
135 }
136 Ok(oof)
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_kfold_covers_every_row_exactly_once() {
145 let folds = kfold_splits(11, 5, 42);
146 let mut seen = vec![0u32; 11];
147 for f in &folds {
148 for &i in &f.val_idx {
149 seen[i] += 1;
150 }
151 assert!(!f.train_idx.is_empty());
152 }
153 assert!(seen.iter().all(|&c| c == 1));
154 }
155
156 #[test]
157 fn test_kfold_fold_count_capped_by_n() {
158 let folds = kfold_splits(3, 5, 42);
159 assert_eq!(folds.len(), 3);
160 }
161}