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(
66 n_features: usize,
67 n_estimators: usize,
68 random_state: u64,
69) -> Vec<Vec<usize>> {
70 let mut rng = PyRandom::new(random_state);
71 if n_features <= 5 {
72 let all_perms = permutations(n_features);
73 let k = n_estimators.min(all_perms.len());
74 rng.sample(&all_perms, k)
75 } else {
76 (0..n_estimators)
77 .map(|_| rng.sample_indices(n_features, n_features))
78 .collect()
79 }
80}
81
82fn permutations(n: usize) -> Vec<Vec<usize>> {
83 let mut items: Vec<usize> = (0..n).collect();
84 let mut result = Vec::new();
85 permute_rec(&mut items, 0, &mut result);
86 result
87}
88
89fn permute_rec(items: &mut Vec<usize>, k: usize, out: &mut Vec<Vec<usize>>) {
90 if k == items.len() {
91 out.push(items.clone());
92 return;
93 }
94 for i in k..items.len() {
95 items.swap(k, i);
96 permute_rec(items, k + 1, out);
97 items.swap(k, i);
98 }
99}
100
101pub fn generate_ensemble(p: &EnsembleConfigParams) -> Vec<MemberConfig> {
108 let shuffle_patterns =
109 generate_feature_permutations(p.n_features, p.n_estimators, p.random_state);
110 let n_members_from_shuffle = shuffle_patterns.len();
111
112 let mut rng = PyRandom::new(p.random_state);
113
114 let shift_offsets: Vec<usize> =
115 if p.is_classification && p.class_shift && p.n_estimators > 1 && p.n_classes > 1 {
116 let base_offsets = rng.sample_indices(p.n_classes, p.n_classes);
117 let num_cycles = p.n_estimators.div_ceil(base_offsets.len());
118 base_offsets
119 .iter()
120 .cycle()
121 .take(base_offsets.len() * num_cycles)
122 .take(p.n_estimators)
123 .copied()
124 .collect()
125 } else {
126 vec![0usize; p.n_estimators]
127 };
128
129 let cat_permutations: Vec<Option<Vec<Vec<usize>>>> =
130 if p.permute_categorical && !p.cat_value_counts.is_empty() {
131 (0..p.n_estimators)
132 .map(|_| {
133 Some(
134 p.cat_value_counts
135 .iter()
136 .map(|&n_vals| rng.sample_indices(n_vals, n_vals))
137 .collect(),
138 )
139 })
140 .collect()
141 } else {
142 vec![None; p.n_estimators]
143 };
144
145 let n_rows_target = p
146 .max_num_rows
147 .map(|m| m.min(p.n_train_rows))
148 .unwrap_or(p.n_train_rows);
149 let row_subsample_patterns: Vec<Option<Vec<usize>>> = if n_rows_target < p.n_train_rows {
150 (0..p.n_estimators)
151 .map(|_| Some(rng.sample_indices(p.n_train_rows, n_rows_target)))
152 .collect()
153 } else {
154 vec![None; p.n_estimators]
155 };
156
157 let n = n_members_from_shuffle.min(shift_offsets.len());
158 let mut zipped: Vec<(
159 Vec<usize>,
160 usize,
161 Option<Vec<Vec<usize>>>,
162 Option<Vec<usize>>,
163 )> = (0..n)
164 .map(|i| {
165 (
166 shuffle_patterns[i].clone(),
167 shift_offsets[i],
168 cat_permutations[i].clone(),
169 row_subsample_patterns[i].clone(),
170 )
171 })
172 .collect();
173 rng.shuffle(&mut zipped);
174
175 let num_cycles = p.n_estimators.div_ceil(p.norm_methods.len());
176 let norm_methods_for_estimators: Vec<NormMethod> = p
177 .norm_methods
178 .iter()
179 .cycle()
180 .take(p.norm_methods.len() * num_cycles)
181 .take(n)
182 .copied()
183 .collect();
184
185 zipped
186 .into_iter()
187 .zip(norm_methods_for_estimators)
188 .map(
189 |((feature_permutation, class_shift, cat_permutation, row_subsample), norm_method)| {
190 MemberConfig {
191 feature_permutation,
192 class_shift,
193 cat_permutation,
194 row_subsample,
195 norm_method,
196 }
197 },
198 )
199 .collect()
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_default_config_shape() {
208 let p = EnsembleConfigParams {
209 n_estimators: 32,
210 n_features: 8,
211 n_train_rows: 10,
212 is_classification: true,
213 n_classes: 3,
214 class_shift: true,
215 permute_categorical: false,
216 cat_value_counts: vec![],
217 max_num_rows: None,
218 norm_methods: vec![NormMethod::None, NormMethod::Power],
219 random_state: 42,
220 };
221 let configs = generate_ensemble(&p);
222 assert_eq!(configs.len(), 32);
223 for (i, c) in configs.iter().enumerate() {
224 assert_eq!(c.feature_permutation.len(), 8);
225 let mut sorted = c.feature_permutation.clone();
226 sorted.sort();
227 assert_eq!(
228 sorted,
229 (0..8).collect::<Vec<_>>(),
230 "member {i} not a valid permutation"
231 );
232 assert!(c.class_shift < 3);
233 assert!(c.cat_permutation.is_none());
234 assert!(c.row_subsample.is_none());
235 }
236 assert_eq!(configs[0].norm_method, NormMethod::None);
238 assert_eq!(configs[1].norm_method, NormMethod::Power);
239 }
240
241 #[test]
242 fn test_small_feature_count_uses_permutation_enumeration() {
243 let p = EnsembleConfigParams {
244 n_estimators: 32,
245 n_features: 3,
246 n_train_rows: 10,
247 is_classification: false,
248 n_classes: 1,
249 class_shift: true,
250 permute_categorical: false,
251 cat_value_counts: vec![],
252 max_num_rows: None,
253 norm_methods: vec![NormMethod::None],
254 random_state: 42,
255 };
256 let configs = generate_ensemble(&p);
257 assert_eq!(configs.len(), 6);
259 }
260
261 #[test]
262 fn test_matches_real_ensemble_generator() {
263 let p = EnsembleConfigParams {
269 n_estimators: 32,
270 n_features: 8,
271 n_train_rows: 10,
272 is_classification: true,
273 n_classes: 3,
274 class_shift: true,
275 permute_categorical: false,
276 cat_value_counts: vec![],
277 max_num_rows: None,
278 norm_methods: vec![NormMethod::None, NormMethod::Power],
279 random_state: 42,
280 };
281 let configs = generate_ensemble(&p);
282 let expected: [(NormMethod, &[usize], usize); 6] = [
283 (NormMethod::None, &[0, 5, 3, 4, 7, 1, 6, 2], 1),
284 (NormMethod::Power, &[3, 0, 1, 4, 6, 7, 5, 2], 0),
285 (NormMethod::None, &[2, 1, 5, 3, 6, 4, 7, 0], 1),
286 (NormMethod::Power, &[4, 1, 6, 7, 2, 3, 5, 0], 2),
287 (NormMethod::None, &[1, 4, 7, 2, 3, 0, 6, 5], 2),
288 (NormMethod::Power, &[2, 3, 1, 7, 6, 0, 4, 5], 0),
289 ];
290 for (i, (exp_method, exp_perm, exp_shift)) in expected.iter().enumerate() {
291 assert_eq!(
292 configs[i].norm_method, *exp_method,
293 "member {i} norm_method"
294 );
295 assert_eq!(
296 &configs[i].feature_permutation, exp_perm,
297 "member {i} permutation"
298 );
299 assert_eq!(configs[i].class_shift, *exp_shift, "member {i} shift");
300 }
301 }
302}