1use super::pyrandom::PyRandom;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub enum NormMethod {
11 None,
12 Power,
13 Quantile,
14 QuantileRtdl,
15 Robust,
16}
17
18impl NormMethod {
19 pub fn parse(s: &str) -> anyhow::Result<Self> {
20 Ok(match s {
21 "none" => NormMethod::None,
22 "power" => NormMethod::Power,
23 "quantile" => NormMethod::Quantile,
24 "quantile_rtdl" => NormMethod::QuantileRtdl,
25 "robust" => NormMethod::Robust,
26 other => anyhow::bail!("unknown norm_method: {other}"),
27 })
28 }
29}
30
31pub struct MemberConfig {
32 pub feature_permutation: Vec<usize>,
35 pub class_shift: usize,
38 pub cat_permutation: Option<Vec<Vec<usize>>>,
41 pub row_subsample: Option<Vec<usize>>,
44 pub norm_method: NormMethod,
45}
46
47pub struct EnsembleConfigParams {
48 pub n_estimators: usize,
49 pub n_features: usize,
50 pub n_train_rows: usize,
51 pub is_classification: bool,
52 pub n_classes: usize,
53 pub class_shift: bool,
54 pub permute_categorical: bool,
55 pub cat_value_counts: Vec<usize>,
58 pub max_num_rows: Option<usize>,
59 pub norm_methods: Vec<NormMethod>,
60 pub random_state: u64,
61}
62
63fn generate_feature_permutations(n_features: usize, n_estimators: usize, random_state: u64) -> Vec<Vec<usize>> {
66 let mut rng = PyRandom::new(random_state);
67 if n_features <= 5 {
68 let all_perms = permutations(n_features);
69 let k = n_estimators.min(all_perms.len());
70 rng.sample(&all_perms, k)
71 } else {
72 (0..n_estimators).map(|_| rng.sample_indices(n_features, n_features)).collect()
73 }
74}
75
76fn permutations(n: usize) -> Vec<Vec<usize>> {
77 let mut items: Vec<usize> = (0..n).collect();
78 let mut result = Vec::new();
79 permute_rec(&mut items, 0, &mut result);
80 result
81}
82
83fn permute_rec(items: &mut Vec<usize>, k: usize, out: &mut Vec<Vec<usize>>) {
84 if k == items.len() {
85 out.push(items.clone());
86 return;
87 }
88 for i in k..items.len() {
89 items.swap(k, i);
90 permute_rec(items, k + 1, out);
91 items.swap(k, i);
92 }
93}
94
95pub fn generate_ensemble(p: &EnsembleConfigParams) -> Vec<MemberConfig> {
102 let shuffle_patterns = generate_feature_permutations(p.n_features, p.n_estimators, p.random_state);
103 let n_members_from_shuffle = shuffle_patterns.len();
104
105 let mut rng = PyRandom::new(p.random_state);
106
107 let shift_offsets: Vec<usize> = if p.is_classification && p.class_shift && p.n_estimators > 1 && p.n_classes > 1 {
108 let base_offsets = rng.sample_indices(p.n_classes, p.n_classes);
109 let num_cycles = p.n_estimators.div_ceil(base_offsets.len());
110 base_offsets
111 .iter()
112 .cycle()
113 .take(base_offsets.len() * num_cycles)
114 .take(p.n_estimators)
115 .copied()
116 .collect()
117 } else {
118 vec![0usize; p.n_estimators]
119 };
120
121 let cat_permutations: Vec<Option<Vec<Vec<usize>>>> = if p.permute_categorical && !p.cat_value_counts.is_empty() {
122 (0..p.n_estimators)
123 .map(|_| {
124 Some(
125 p.cat_value_counts
126 .iter()
127 .map(|&n_vals| rng.sample_indices(n_vals, n_vals))
128 .collect(),
129 )
130 })
131 .collect()
132 } else {
133 vec![None; p.n_estimators]
134 };
135
136 let n_rows_target = p.max_num_rows.map(|m| m.min(p.n_train_rows)).unwrap_or(p.n_train_rows);
137 let row_subsample_patterns: Vec<Option<Vec<usize>>> = if n_rows_target < p.n_train_rows {
138 (0..p.n_estimators).map(|_| Some(rng.sample_indices(p.n_train_rows, n_rows_target))).collect()
139 } else {
140 vec![None; p.n_estimators]
141 };
142
143 let n = n_members_from_shuffle.min(shift_offsets.len());
144 let mut zipped: Vec<(Vec<usize>, usize, Option<Vec<Vec<usize>>>, Option<Vec<usize>>)> = (0..n)
145 .map(|i| {
146 (
147 shuffle_patterns[i].clone(),
148 shift_offsets[i],
149 cat_permutations[i].clone(),
150 row_subsample_patterns[i].clone(),
151 )
152 })
153 .collect();
154 rng.shuffle(&mut zipped);
155
156 let num_cycles = p.n_estimators.div_ceil(p.norm_methods.len());
157 let norm_methods_for_estimators: Vec<NormMethod> =
158 p.norm_methods.iter().cycle().take(p.norm_methods.len() * num_cycles).take(n).copied().collect();
159
160 zipped
161 .into_iter()
162 .zip(norm_methods_for_estimators)
163 .map(|((feature_permutation, class_shift, cat_permutation, row_subsample), norm_method)| MemberConfig {
164 feature_permutation,
165 class_shift,
166 cat_permutation,
167 row_subsample,
168 norm_method,
169 })
170 .collect()
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_default_config_shape() {
179 let p = EnsembleConfigParams {
180 n_estimators: 32,
181 n_features: 8,
182 n_train_rows: 10,
183 is_classification: true,
184 n_classes: 3,
185 class_shift: true,
186 permute_categorical: false,
187 cat_value_counts: vec![],
188 max_num_rows: None,
189 norm_methods: vec![NormMethod::None, NormMethod::Power],
190 random_state: 42,
191 };
192 let configs = generate_ensemble(&p);
193 assert_eq!(configs.len(), 32);
194 for (i, c) in configs.iter().enumerate() {
195 assert_eq!(c.feature_permutation.len(), 8);
196 let mut sorted = c.feature_permutation.clone();
197 sorted.sort();
198 assert_eq!(sorted, (0..8).collect::<Vec<_>>(), "member {i} not a valid permutation");
199 assert!(c.class_shift < 3);
200 assert!(c.cat_permutation.is_none());
201 assert!(c.row_subsample.is_none());
202 }
203 assert_eq!(configs[0].norm_method, NormMethod::None);
205 assert_eq!(configs[1].norm_method, NormMethod::Power);
206 }
207
208 #[test]
209 fn test_small_feature_count_uses_permutation_enumeration() {
210 let p = EnsembleConfigParams {
211 n_estimators: 32,
212 n_features: 3,
213 n_train_rows: 10,
214 is_classification: false,
215 n_classes: 1,
216 class_shift: true,
217 permute_categorical: false,
218 cat_value_counts: vec![],
219 max_num_rows: None,
220 norm_methods: vec![NormMethod::None],
221 random_state: 42,
222 };
223 let configs = generate_ensemble(&p);
224 assert_eq!(configs.len(), 6);
226 }
227
228 #[test]
229 fn test_matches_real_ensemble_generator() {
230 let p = EnsembleConfigParams {
236 n_estimators: 32,
237 n_features: 8,
238 n_train_rows: 10,
239 is_classification: true,
240 n_classes: 3,
241 class_shift: true,
242 permute_categorical: false,
243 cat_value_counts: vec![],
244 max_num_rows: None,
245 norm_methods: vec![NormMethod::None, NormMethod::Power],
246 random_state: 42,
247 };
248 let configs = generate_ensemble(&p);
249 let expected: [(NormMethod, &[usize], usize); 6] = [
250 (NormMethod::None, &[0, 5, 3, 4, 7, 1, 6, 2], 1),
251 (NormMethod::Power, &[3, 0, 1, 4, 6, 7, 5, 2], 0),
252 (NormMethod::None, &[2, 1, 5, 3, 6, 4, 7, 0], 1),
253 (NormMethod::Power, &[4, 1, 6, 7, 2, 3, 5, 0], 2),
254 (NormMethod::None, &[1, 4, 7, 2, 3, 0, 6, 5], 2),
255 (NormMethod::Power, &[2, 3, 1, 7, 6, 0, 4, 5], 0),
256 ];
257 for (i, (exp_method, exp_perm, exp_shift)) in expected.iter().enumerate() {
258 assert_eq!(configs[i].norm_method, *exp_method, "member {i} norm_method");
259 assert_eq!(&configs[i].feature_permutation, exp_perm, "member {i} permutation");
260 assert_eq!(configs[i].class_shift, *exp_shift, "member {i} shift");
261 }
262 }
263}