1use ferrolearn_core::error::FerroError;
54use ferrolearn_core::introspection::{HasClasses, HasFeatureImportances};
55use ferrolearn_core::pipeline::{FittedPipelineEstimator, PipelineEstimator};
56use ferrolearn_core::traits::{Fit, Predict};
57use ndarray::{Array1, Array2};
58use num_traits::{Float, FromPrimitive, ToPrimitive};
59use rand::SeedableRng;
60use rand::rngs::StdRng;
61use rayon::prelude::*;
62use serde::{Deserialize, Serialize};
63
64use crate::decision_tree::{
65 self, ClassificationCriterion, Node, build_classification_tree_per_split_features,
66 build_regression_tree_per_split_features, compute_feature_importances,
67};
68
69#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
75pub enum MaxFeatures {
76 Sqrt,
78 Log2,
80 All,
82 Fixed(usize),
84 Fraction(f64),
86}
87
88fn resolve_max_features(strategy: MaxFeatures, n_features: usize) -> usize {
90 let result = match strategy {
91 MaxFeatures::Sqrt => (n_features as f64).sqrt().ceil() as usize,
92 MaxFeatures::Log2 => (n_features as f64).log2().ceil().max(1.0) as usize,
93 MaxFeatures::All => n_features,
94 MaxFeatures::Fixed(n) => n.min(n_features),
95 MaxFeatures::Fraction(f) => ((n_features as f64) * f).ceil() as usize,
96 };
97 result.max(1).min(n_features)
98}
99
100fn make_tree_params(
105 max_depth: Option<usize>,
106 min_samples_split: usize,
107 min_samples_leaf: usize,
108) -> decision_tree::TreeParams {
109 decision_tree::TreeParams {
110 max_depth,
111 min_samples_split,
112 min_samples_leaf,
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct RandomForestClassifier<F> {
131 pub n_estimators: usize,
133 pub max_depth: Option<usize>,
135 pub max_features: MaxFeatures,
137 pub min_samples_split: usize,
139 pub min_samples_leaf: usize,
141 pub random_state: Option<u64>,
143 pub criterion: ClassificationCriterion,
145 _marker: std::marker::PhantomData<F>,
146}
147
148impl<F: Float> RandomForestClassifier<F> {
149 #[must_use]
156 pub fn new() -> Self {
157 Self {
158 n_estimators: 100,
159 max_depth: None,
160 max_features: MaxFeatures::Sqrt,
161 min_samples_split: 2,
162 min_samples_leaf: 1,
163 random_state: None,
164 criterion: ClassificationCriterion::Gini,
165 _marker: std::marker::PhantomData,
166 }
167 }
168
169 #[must_use]
171 pub fn with_n_estimators(mut self, n_estimators: usize) -> Self {
172 self.n_estimators = n_estimators;
173 self
174 }
175
176 #[must_use]
178 pub fn with_max_depth(mut self, max_depth: Option<usize>) -> Self {
179 self.max_depth = max_depth;
180 self
181 }
182
183 #[must_use]
185 pub fn with_max_features(mut self, max_features: MaxFeatures) -> Self {
186 self.max_features = max_features;
187 self
188 }
189
190 #[must_use]
192 pub fn with_min_samples_split(mut self, min_samples_split: usize) -> Self {
193 self.min_samples_split = min_samples_split;
194 self
195 }
196
197 #[must_use]
199 pub fn with_min_samples_leaf(mut self, min_samples_leaf: usize) -> Self {
200 self.min_samples_leaf = min_samples_leaf;
201 self
202 }
203
204 #[must_use]
206 pub fn with_random_state(mut self, seed: u64) -> Self {
207 self.random_state = Some(seed);
208 self
209 }
210
211 #[must_use]
213 pub fn with_criterion(mut self, criterion: ClassificationCriterion) -> Self {
214 self.criterion = criterion;
215 self
216 }
217}
218
219impl<F: Float> Default for RandomForestClassifier<F> {
220 fn default() -> Self {
221 Self::new()
222 }
223}
224
225#[derive(Debug, Clone)]
234pub struct FittedRandomForestClassifier<F> {
235 trees: Vec<Vec<Node<F>>>,
237 classes: Vec<usize>,
239 n_features: usize,
241 feature_importances: Array1<F>,
243}
244
245impl<F: Float + Send + Sync + 'static> Fit<Array2<F>, Array1<usize>> for RandomForestClassifier<F> {
246 type Fitted = FittedRandomForestClassifier<F>;
247 type Error = FerroError;
248
249 fn fit(
261 &self,
262 x: &Array2<F>,
263 y: &Array1<usize>,
264 ) -> Result<FittedRandomForestClassifier<F>, FerroError> {
265 let (n_samples, n_features) = x.dim();
266
267 if n_samples != y.len() {
268 return Err(FerroError::ShapeMismatch {
269 expected: vec![n_samples],
270 actual: vec![y.len()],
271 context: "y length must match number of samples in X".into(),
272 });
273 }
274 if n_samples == 0 {
275 return Err(FerroError::InsufficientSamples {
276 required: 1,
277 actual: 0,
278 context: "RandomForestClassifier requires at least one sample".into(),
279 });
280 }
281 if self.n_estimators == 0 {
282 return Err(FerroError::InvalidParameter {
283 name: "n_estimators".into(),
284 reason: "must be at least 1".into(),
285 });
286 }
287
288 let mut classes: Vec<usize> = y.iter().copied().collect();
290 classes.sort_unstable();
291 classes.dedup();
292 let n_classes = classes.len();
293
294 let y_mapped: Vec<usize> = y
295 .iter()
296 .map(|&c| classes.iter().position(|&cl| cl == c).unwrap())
297 .collect();
298
299 let max_features_n = resolve_max_features(self.max_features, n_features);
300 let params = make_tree_params(
301 self.max_depth,
302 self.min_samples_split,
303 self.min_samples_leaf,
304 );
305 let criterion = self.criterion;
306
307 let tree_seeds: Vec<u64> = if let Some(seed) = self.random_state {
309 let mut master_rng = StdRng::seed_from_u64(seed);
310 (0..self.n_estimators)
311 .map(|_| {
312 use rand::RngCore;
313 master_rng.next_u64()
314 })
315 .collect()
316 } else {
317 (0..self.n_estimators)
318 .map(|_| {
319 use rand::RngCore;
320 rand::rng().next_u64()
321 })
322 .collect()
323 };
324
325 let trees: Vec<Vec<Node<F>>> = tree_seeds
335 .par_iter()
336 .map(|&seed| {
337 let mut bootstrap_rng = StdRng::seed_from_u64(seed);
338
339 let bootstrap_indices: Vec<usize> = (0..n_samples)
340 .map(|_| {
341 use rand::RngCore;
342 (bootstrap_rng.next_u64() as usize) % n_samples
343 })
344 .collect();
345
346 use rand::RngCore;
350 let split_seed = bootstrap_rng.next_u64();
351
352 build_classification_tree_per_split_features(
353 x,
354 &y_mapped,
355 n_classes,
356 &bootstrap_indices,
357 max_features_n,
358 ¶ms,
359 criterion,
360 split_seed,
361 )
362 })
363 .collect();
364
365 let mut total_importances = Array1::<F>::zeros(n_features);
367 for tree_nodes in &trees {
368 let tree_imp = compute_feature_importances(tree_nodes, n_features, n_samples);
369 total_importances = total_importances + tree_imp;
370 }
371 let imp_sum: F = total_importances
372 .iter()
373 .copied()
374 .fold(F::zero(), |a, b| a + b);
375 if imp_sum > F::zero() {
376 total_importances.mapv_inplace(|v| v / imp_sum);
377 }
378
379 Ok(FittedRandomForestClassifier {
380 trees,
381 classes,
382 n_features,
383 feature_importances: total_importances,
384 })
385 }
386}
387
388impl<F: Float + Send + Sync + 'static> FittedRandomForestClassifier<F> {
389 #[must_use]
391 pub fn trees(&self) -> &[Vec<Node<F>>] {
392 &self.trees
393 }
394
395 #[must_use]
397 pub fn n_features(&self) -> usize {
398 self.n_features
399 }
400
401 pub fn score(&self, x: &Array2<F>, y: &Array1<usize>) -> Result<F, FerroError> {
409 if x.nrows() != y.len() {
410 return Err(FerroError::ShapeMismatch {
411 expected: vec![x.nrows()],
412 actual: vec![y.len()],
413 context: "y length must match number of samples in X".into(),
414 });
415 }
416 let preds = self.predict(x)?;
417 Ok(crate::mean_accuracy(&preds, y))
418 }
419
420 pub fn predict_proba(&self, x: &Array2<F>) -> Result<Array2<F>, FerroError> {
433 if x.ncols() != self.n_features {
434 return Err(FerroError::ShapeMismatch {
435 expected: vec![self.n_features],
436 actual: vec![x.ncols()],
437 context: "number of features must match fitted model".into(),
438 });
439 }
440 let n_samples = x.nrows();
441 let n_classes = self.classes.len();
442 let n_trees_f = F::from(self.trees.len()).unwrap();
443 let mut proba = Array2::<F>::zeros((n_samples, n_classes));
444
445 for i in 0..n_samples {
446 let row = x.row(i);
447 for tree_nodes in &self.trees {
448 let leaf_idx = decision_tree::traverse(tree_nodes, &row);
449 match &tree_nodes[leaf_idx] {
450 Node::Leaf {
451 class_distribution: Some(dist),
452 ..
453 } => {
454 for (j, &p) in dist.iter().enumerate().take(n_classes) {
455 proba[[i, j]] = proba[[i, j]] + p;
456 }
457 }
458 Node::Leaf { value, .. } => {
459 let class_idx = value.to_f64().map_or(0, |f| f.round() as usize);
460 if class_idx < n_classes {
461 proba[[i, class_idx]] = proba[[i, class_idx]] + F::one();
462 }
463 }
464 _ => {}
465 }
466 }
467 for j in 0..n_classes {
468 proba[[i, j]] = proba[[i, j]] / n_trees_f;
469 }
470 }
471 Ok(proba)
472 }
473
474 pub fn predict_log_proba(&self, x: &Array2<F>) -> Result<Array2<F>, FerroError> {
481 let proba = self.predict_proba(x)?;
482 Ok(crate::log_proba(&proba))
483 }
484}
485
486impl<F: Float + Send + Sync + 'static> Predict<Array2<F>> for FittedRandomForestClassifier<F> {
487 type Output = Array1<usize>;
488 type Error = FerroError;
489
490 fn predict(&self, x: &Array2<F>) -> Result<Array1<usize>, FerroError> {
505 let proba = self.predict_proba(x)?;
506 let n_samples = proba.nrows();
507 let n_classes = proba.ncols();
508 let mut predictions = Array1::zeros(n_samples);
509
510 for i in 0..n_samples {
511 let mut best = 0usize;
513 let mut best_v = proba[[i, 0]];
514 for j in 1..n_classes {
515 let v = proba[[i, j]];
516 if v > best_v {
517 best_v = v;
518 best = j;
519 }
520 }
521 predictions[i] = self.classes[best];
522 }
523
524 Ok(predictions)
525 }
526}
527
528impl<F: Float + Send + Sync + 'static> HasFeatureImportances<F>
529 for FittedRandomForestClassifier<F>
530{
531 fn feature_importances(&self) -> &Array1<F> {
532 &self.feature_importances
533 }
534}
535
536impl<F: Float + Send + Sync + 'static> HasClasses for FittedRandomForestClassifier<F> {
537 fn classes(&self) -> &[usize] {
538 &self.classes
539 }
540
541 fn n_classes(&self) -> usize {
542 self.classes.len()
543 }
544}
545
546impl<F: Float + ToPrimitive + FromPrimitive + Send + Sync + 'static> PipelineEstimator<F>
548 for RandomForestClassifier<F>
549{
550 fn fit_pipeline(
551 &self,
552 x: &Array2<F>,
553 y: &Array1<F>,
554 ) -> Result<Box<dyn FittedPipelineEstimator<F>>, FerroError> {
555 let y_usize: Array1<usize> = y.mapv(|v| v.to_usize().unwrap_or(0));
556 let fitted = self.fit(x, &y_usize)?;
557 Ok(Box::new(FittedForestClassifierPipelineAdapter(fitted)))
558 }
559}
560
561struct FittedForestClassifierPipelineAdapter<F: Float + Send + Sync + 'static>(
563 FittedRandomForestClassifier<F>,
564);
565
566impl<F: Float + ToPrimitive + FromPrimitive + Send + Sync + 'static> FittedPipelineEstimator<F>
567 for FittedForestClassifierPipelineAdapter<F>
568{
569 fn predict_pipeline(&self, x: &Array2<F>) -> Result<Array1<F>, FerroError> {
570 let preds = self.0.predict(x)?;
571 Ok(preds.mapv(|v| F::from_usize(v).unwrap_or_else(F::nan)))
572 }
573}
574
575#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct RandomForestRegressor<F> {
590 pub n_estimators: usize,
592 pub max_depth: Option<usize>,
594 pub max_features: MaxFeatures,
596 pub min_samples_split: usize,
598 pub min_samples_leaf: usize,
600 pub random_state: Option<u64>,
602 _marker: std::marker::PhantomData<F>,
603}
604
605impl<F: Float> RandomForestRegressor<F> {
606 #[must_use]
612 pub fn new() -> Self {
613 Self {
614 n_estimators: 100,
615 max_depth: None,
616 max_features: MaxFeatures::All,
617 min_samples_split: 2,
618 min_samples_leaf: 1,
619 random_state: None,
620 _marker: std::marker::PhantomData,
621 }
622 }
623
624 #[must_use]
626 pub fn with_n_estimators(mut self, n_estimators: usize) -> Self {
627 self.n_estimators = n_estimators;
628 self
629 }
630
631 #[must_use]
633 pub fn with_max_depth(mut self, max_depth: Option<usize>) -> Self {
634 self.max_depth = max_depth;
635 self
636 }
637
638 #[must_use]
640 pub fn with_max_features(mut self, max_features: MaxFeatures) -> Self {
641 self.max_features = max_features;
642 self
643 }
644
645 #[must_use]
647 pub fn with_min_samples_split(mut self, min_samples_split: usize) -> Self {
648 self.min_samples_split = min_samples_split;
649 self
650 }
651
652 #[must_use]
654 pub fn with_min_samples_leaf(mut self, min_samples_leaf: usize) -> Self {
655 self.min_samples_leaf = min_samples_leaf;
656 self
657 }
658
659 #[must_use]
661 pub fn with_random_state(mut self, seed: u64) -> Self {
662 self.random_state = Some(seed);
663 self
664 }
665}
666
667impl<F: Float> Default for RandomForestRegressor<F> {
668 fn default() -> Self {
669 Self::new()
670 }
671}
672
673#[derive(Debug, Clone)]
682pub struct FittedRandomForestRegressor<F> {
683 trees: Vec<Vec<Node<F>>>,
685 n_features: usize,
687 feature_importances: Array1<F>,
689}
690
691impl<F: Float + Send + Sync + 'static> Fit<Array2<F>, Array1<F>> for RandomForestRegressor<F> {
692 type Fitted = FittedRandomForestRegressor<F>;
693 type Error = FerroError;
694
695 fn fit(
704 &self,
705 x: &Array2<F>,
706 y: &Array1<F>,
707 ) -> Result<FittedRandomForestRegressor<F>, FerroError> {
708 let (n_samples, n_features) = x.dim();
709
710 if n_samples != y.len() {
711 return Err(FerroError::ShapeMismatch {
712 expected: vec![n_samples],
713 actual: vec![y.len()],
714 context: "y length must match number of samples in X".into(),
715 });
716 }
717 if n_samples == 0 {
718 return Err(FerroError::InsufficientSamples {
719 required: 1,
720 actual: 0,
721 context: "RandomForestRegressor requires at least one sample".into(),
722 });
723 }
724 if self.n_estimators == 0 {
725 return Err(FerroError::InvalidParameter {
726 name: "n_estimators".into(),
727 reason: "must be at least 1".into(),
728 });
729 }
730
731 let max_features_n = resolve_max_features(self.max_features, n_features);
732 let params = make_tree_params(
733 self.max_depth,
734 self.min_samples_split,
735 self.min_samples_leaf,
736 );
737
738 let tree_seeds: Vec<u64> = if let Some(seed) = self.random_state {
740 let mut master_rng = StdRng::seed_from_u64(seed);
741 (0..self.n_estimators)
742 .map(|_| {
743 use rand::RngCore;
744 master_rng.next_u64()
745 })
746 .collect()
747 } else {
748 (0..self.n_estimators)
749 .map(|_| {
750 use rand::RngCore;
751 rand::rng().next_u64()
752 })
753 .collect()
754 };
755
756 let trees: Vec<Vec<Node<F>>> = tree_seeds
759 .par_iter()
760 .map(|&seed| {
761 let mut bootstrap_rng = StdRng::seed_from_u64(seed);
762
763 let bootstrap_indices: Vec<usize> = (0..n_samples)
764 .map(|_| {
765 use rand::RngCore;
766 (bootstrap_rng.next_u64() as usize) % n_samples
767 })
768 .collect();
769
770 use rand::RngCore;
771 let split_seed = bootstrap_rng.next_u64();
772
773 build_regression_tree_per_split_features(
774 x,
775 y,
776 &bootstrap_indices,
777 max_features_n,
778 ¶ms,
779 split_seed,
780 )
781 })
782 .collect();
783
784 let mut total_importances = Array1::<F>::zeros(n_features);
786 for tree_nodes in &trees {
787 let tree_imp = compute_feature_importances(tree_nodes, n_features, n_samples);
788 total_importances = total_importances + tree_imp;
789 }
790 let imp_sum: F = total_importances
791 .iter()
792 .copied()
793 .fold(F::zero(), |a, b| a + b);
794 if imp_sum > F::zero() {
795 total_importances.mapv_inplace(|v| v / imp_sum);
796 }
797
798 Ok(FittedRandomForestRegressor {
799 trees,
800 n_features,
801 feature_importances: total_importances,
802 })
803 }
804}
805
806impl<F: Float + Send + Sync + 'static> FittedRandomForestRegressor<F> {
807 #[must_use]
809 pub fn trees(&self) -> &[Vec<Node<F>>] {
810 &self.trees
811 }
812
813 #[must_use]
815 pub fn n_features(&self) -> usize {
816 self.n_features
817 }
818
819 pub fn score(&self, x: &Array2<F>, y: &Array1<F>) -> Result<F, FerroError> {
827 if x.nrows() != y.len() {
828 return Err(FerroError::ShapeMismatch {
829 expected: vec![x.nrows()],
830 actual: vec![y.len()],
831 context: "y length must match number of samples in X".into(),
832 });
833 }
834 let preds = self.predict(x)?;
835 Ok(crate::r2_score(&preds, y))
836 }
837}
838
839impl<F: Float + Send + Sync + 'static> Predict<Array2<F>> for FittedRandomForestRegressor<F> {
840 type Output = Array1<F>;
841 type Error = FerroError;
842
843 fn predict(&self, x: &Array2<F>) -> Result<Array1<F>, FerroError> {
850 if x.ncols() != self.n_features {
851 return Err(FerroError::ShapeMismatch {
852 expected: vec![self.n_features],
853 actual: vec![x.ncols()],
854 context: "number of features must match fitted model".into(),
855 });
856 }
857
858 let n_samples = x.nrows();
859 let n_trees_f = F::from(self.trees.len()).unwrap();
860 let mut predictions = Array1::zeros(n_samples);
861
862 for i in 0..n_samples {
863 let row = x.row(i);
864 let mut sum = F::zero();
865
866 for tree_nodes in &self.trees {
867 let leaf_idx = decision_tree::traverse(tree_nodes, &row);
868 if let Node::Leaf { value, .. } = tree_nodes[leaf_idx] {
869 sum = sum + value;
870 }
871 }
872
873 predictions[i] = sum / n_trees_f;
874 }
875
876 Ok(predictions)
877 }
878}
879
880impl<F: Float + Send + Sync + 'static> HasFeatureImportances<F> for FittedRandomForestRegressor<F> {
881 fn feature_importances(&self) -> &Array1<F> {
882 &self.feature_importances
883 }
884}
885
886impl<F: Float + Send + Sync + 'static> PipelineEstimator<F> for RandomForestRegressor<F> {
888 fn fit_pipeline(
889 &self,
890 x: &Array2<F>,
891 y: &Array1<F>,
892 ) -> Result<Box<dyn FittedPipelineEstimator<F>>, FerroError> {
893 let fitted = self.fit(x, y)?;
894 Ok(Box::new(fitted))
895 }
896}
897
898impl<F: Float + Send + Sync + 'static> FittedPipelineEstimator<F>
899 for FittedRandomForestRegressor<F>
900{
901 fn predict_pipeline(&self, x: &Array2<F>) -> Result<Array1<F>, FerroError> {
902 self.predict(x)
903 }
904}
905
906#[cfg(test)]
911mod tests {
912 use super::*;
913 use approx::assert_relative_eq;
914 use ndarray::array;
915
916 #[test]
919 fn test_forest_classifier_simple() {
920 let x = Array2::from_shape_vec(
921 (8, 2),
922 vec![
923 1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 4.0, 5.0, 6.0, 6.0, 7.0, 7.0, 8.0, 8.0, 9.0,
924 ],
925 )
926 .unwrap();
927 let y = array![0, 0, 0, 0, 1, 1, 1, 1];
928
929 let model = RandomForestClassifier::<f64>::new()
930 .with_n_estimators(20)
931 .with_random_state(42);
932 let fitted = model.fit(&x, &y).unwrap();
933 let preds = fitted.predict(&x).unwrap();
934
935 assert_eq!(preds.len(), 8);
936 for i in 0..4 {
937 assert_eq!(preds[i], 0);
938 }
939 for i in 4..8 {
940 assert_eq!(preds[i], 1);
941 }
942 }
943
944 #[test]
945 fn test_forest_classifier_reproducibility() {
946 let x = Array2::from_shape_vec(
947 (8, 2),
948 vec![
949 1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 4.0, 5.0, 6.0, 6.0, 7.0, 7.0, 8.0, 8.0, 9.0,
950 ],
951 )
952 .unwrap();
953 let y = array![0, 0, 0, 0, 1, 1, 1, 1];
954
955 let model = RandomForestClassifier::<f64>::new()
956 .with_n_estimators(10)
957 .with_random_state(123);
958
959 let fitted1 = model.fit(&x, &y).unwrap();
960 let fitted2 = model.fit(&x, &y).unwrap();
961
962 let preds1 = fitted1.predict(&x).unwrap();
963 let preds2 = fitted2.predict(&x).unwrap();
964
965 assert_eq!(preds1, preds2);
966 }
967
968 #[test]
969 fn test_forest_classifier_feature_importances() {
970 let x = Array2::from_shape_vec(
971 (10, 3),
972 vec![
973 1.0, 0.0, 0.0, 2.0, 0.0, 0.0, 3.0, 0.0, 0.0, 4.0, 0.0, 0.0, 5.0, 0.0, 0.0, 6.0,
974 0.0, 0.0, 7.0, 0.0, 0.0, 8.0, 0.0, 0.0, 9.0, 0.0, 0.0, 10.0, 0.0, 0.0,
975 ],
976 )
977 .unwrap();
978 let y = array![0, 0, 0, 0, 0, 1, 1, 1, 1, 1];
979
980 let model = RandomForestClassifier::<f64>::new()
981 .with_n_estimators(20)
982 .with_max_features(MaxFeatures::All)
983 .with_random_state(42);
984 let fitted = model.fit(&x, &y).unwrap();
985 let importances = fitted.feature_importances();
986
987 assert_eq!(importances.len(), 3);
988 assert!(importances[0] > importances[1]);
989 assert!(importances[0] > importances[2]);
990 }
991
992 #[test]
993 fn test_forest_classifier_has_classes() {
994 let x = Array2::from_shape_vec((6, 1), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
995 let y = array![0, 1, 2, 0, 1, 2];
996
997 let model = RandomForestClassifier::<f64>::new()
998 .with_n_estimators(5)
999 .with_random_state(0);
1000 let fitted = model.fit(&x, &y).unwrap();
1001
1002 assert_eq!(fitted.classes(), &[0, 1, 2]);
1003 assert_eq!(fitted.n_classes(), 3);
1004 }
1005
1006 #[test]
1007 fn test_forest_classifier_shape_mismatch_fit() {
1008 let x = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
1009 let y = array![0, 1];
1010
1011 let model = RandomForestClassifier::<f64>::new().with_n_estimators(5);
1012 assert!(model.fit(&x, &y).is_err());
1013 }
1014
1015 #[test]
1016 fn test_forest_classifier_shape_mismatch_predict() {
1017 let x =
1018 Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).unwrap();
1019 let y = array![0, 0, 1, 1];
1020
1021 let model = RandomForestClassifier::<f64>::new()
1022 .with_n_estimators(5)
1023 .with_random_state(0);
1024 let fitted = model.fit(&x, &y).unwrap();
1025
1026 let x_bad = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
1027 assert!(fitted.predict(&x_bad).is_err());
1028 }
1029
1030 #[test]
1031 fn test_forest_classifier_empty_data() {
1032 let x = Array2::<f64>::zeros((0, 2));
1033 let y = Array1::<usize>::zeros(0);
1034
1035 let model = RandomForestClassifier::<f64>::new().with_n_estimators(5);
1036 assert!(model.fit(&x, &y).is_err());
1037 }
1038
1039 #[test]
1040 fn test_forest_classifier_zero_estimators() {
1041 let x = Array2::from_shape_vec((4, 1), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
1042 let y = array![0, 0, 1, 1];
1043
1044 let model = RandomForestClassifier::<f64>::new().with_n_estimators(0);
1045 assert!(model.fit(&x, &y).is_err());
1046 }
1047
1048 #[test]
1049 fn test_forest_classifier_single_tree() {
1050 let x = Array2::from_shape_vec((6, 1), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
1051 let y = array![0, 0, 0, 1, 1, 1];
1052
1053 let model = RandomForestClassifier::<f64>::new()
1054 .with_n_estimators(1)
1055 .with_max_features(MaxFeatures::All)
1056 .with_random_state(42);
1057 let fitted = model.fit(&x, &y).unwrap();
1058 let preds = fitted.predict(&x).unwrap();
1059
1060 assert_eq!(preds.len(), 6);
1061 }
1062
1063 #[test]
1064 fn test_forest_classifier_pipeline_integration() {
1065 let x = Array2::from_shape_vec((6, 1), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
1066 let y = Array1::from_vec(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
1067
1068 let model = RandomForestClassifier::<f64>::new()
1069 .with_n_estimators(5)
1070 .with_random_state(42);
1071 let fitted = model.fit_pipeline(&x, &y).unwrap();
1072 let preds = fitted.predict_pipeline(&x).unwrap();
1073 assert_eq!(preds.len(), 6);
1074 }
1075
1076 #[test]
1077 fn test_forest_classifier_max_depth() {
1078 let x =
1079 Array2::from_shape_vec((8, 1), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).unwrap();
1080 let y = array![0, 0, 0, 0, 1, 1, 1, 1];
1081
1082 let model = RandomForestClassifier::<f64>::new()
1083 .with_n_estimators(10)
1084 .with_max_depth(Some(1))
1085 .with_max_features(MaxFeatures::All)
1086 .with_random_state(42);
1087 let fitted = model.fit(&x, &y).unwrap();
1088 let preds = fitted.predict(&x).unwrap();
1089
1090 assert_eq!(preds.len(), 8);
1091 }
1092
1093 #[test]
1096 fn test_forest_regressor_simple() {
1097 let x =
1098 Array2::from_shape_vec((8, 1), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).unwrap();
1099 let y = array![1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0];
1100
1101 let model = RandomForestRegressor::<f64>::new()
1102 .with_n_estimators(50)
1103 .with_random_state(42);
1104 let fitted = model.fit(&x, &y).unwrap();
1105 let preds = fitted.predict(&x).unwrap();
1106
1107 assert_eq!(preds.len(), 8);
1108 for i in 0..4 {
1109 assert!(preds[i] < 3.0, "Expected ~1.0, got {}", preds[i]);
1110 }
1111 for i in 4..8 {
1112 assert!(preds[i] > 3.0, "Expected ~5.0, got {}", preds[i]);
1113 }
1114 }
1115
1116 #[test]
1117 fn test_forest_regressor_reproducibility() {
1118 let x = Array2::from_shape_vec((6, 1), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
1119 let y = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1120
1121 let model = RandomForestRegressor::<f64>::new()
1122 .with_n_estimators(10)
1123 .with_random_state(99);
1124
1125 let fitted1 = model.fit(&x, &y).unwrap();
1126 let fitted2 = model.fit(&x, &y).unwrap();
1127
1128 let preds1 = fitted1.predict(&x).unwrap();
1129 let preds2 = fitted2.predict(&x).unwrap();
1130
1131 for (p1, p2) in preds1.iter().zip(preds2.iter()) {
1132 assert_relative_eq!(*p1, *p2, epsilon = 1e-10);
1133 }
1134 }
1135
1136 #[test]
1137 fn test_forest_regressor_feature_importances() {
1138 let x = Array2::from_shape_vec(
1139 (8, 2),
1140 vec![
1141 1.0, 0.0, 2.0, 0.0, 3.0, 0.0, 4.0, 0.0, 5.0, 0.0, 6.0, 0.0, 7.0, 0.0, 8.0, 0.0,
1142 ],
1143 )
1144 .unwrap();
1145 let y = array![1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0];
1146
1147 let model = RandomForestRegressor::<f64>::new()
1148 .with_n_estimators(20)
1149 .with_max_features(MaxFeatures::All)
1150 .with_random_state(42);
1151 let fitted = model.fit(&x, &y).unwrap();
1152 let importances = fitted.feature_importances();
1153
1154 assert_eq!(importances.len(), 2);
1155 assert!(importances[0] > importances[1]);
1156 }
1157
1158 #[test]
1159 fn test_forest_regressor_shape_mismatch_fit() {
1160 let x = Array2::from_shape_vec((3, 1), vec![1.0, 2.0, 3.0]).unwrap();
1161 let y = array![1.0, 2.0];
1162
1163 let model = RandomForestRegressor::<f64>::new().with_n_estimators(5);
1164 assert!(model.fit(&x, &y).is_err());
1165 }
1166
1167 #[test]
1168 fn test_forest_regressor_shape_mismatch_predict() {
1169 let x =
1170 Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).unwrap();
1171 let y = array![1.0, 2.0, 3.0, 4.0];
1172
1173 let model = RandomForestRegressor::<f64>::new()
1174 .with_n_estimators(5)
1175 .with_random_state(0);
1176 let fitted = model.fit(&x, &y).unwrap();
1177
1178 let x_bad = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
1179 assert!(fitted.predict(&x_bad).is_err());
1180 }
1181
1182 #[test]
1183 fn test_forest_regressor_empty_data() {
1184 let x = Array2::<f64>::zeros((0, 2));
1185 let y = Array1::<f64>::zeros(0);
1186
1187 let model = RandomForestRegressor::<f64>::new().with_n_estimators(5);
1188 assert!(model.fit(&x, &y).is_err());
1189 }
1190
1191 #[test]
1192 fn test_forest_regressor_zero_estimators() {
1193 let x = Array2::from_shape_vec((4, 1), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
1194 let y = array![1.0, 2.0, 3.0, 4.0];
1195
1196 let model = RandomForestRegressor::<f64>::new().with_n_estimators(0);
1197 assert!(model.fit(&x, &y).is_err());
1198 }
1199
1200 #[test]
1201 fn test_forest_regressor_pipeline_integration() {
1202 let x = Array2::from_shape_vec((4, 1), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
1203 let y = array![1.0, 2.0, 3.0, 4.0];
1204
1205 let model = RandomForestRegressor::<f64>::new()
1206 .with_n_estimators(5)
1207 .with_random_state(42);
1208 let fitted = model.fit_pipeline(&x, &y).unwrap();
1209 let preds = fitted.predict_pipeline(&x).unwrap();
1210 assert_eq!(preds.len(), 4);
1211 }
1212
1213 #[test]
1214 fn test_forest_regressor_max_features_strategies() {
1215 let x = Array2::from_shape_vec(
1216 (8, 4),
1217 vec![
1218 1.0, 2.0, 3.0, 4.0, 2.0, 3.0, 4.0, 5.0, 3.0, 4.0, 5.0, 6.0, 4.0, 5.0, 6.0, 7.0,
1219 5.0, 6.0, 7.0, 8.0, 6.0, 7.0, 8.0, 9.0, 7.0, 8.0, 9.0, 10.0, 8.0, 9.0, 10.0, 11.0,
1220 ],
1221 )
1222 .unwrap();
1223 let y = array![1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0];
1224
1225 for strategy in &[
1226 MaxFeatures::Sqrt,
1227 MaxFeatures::Log2,
1228 MaxFeatures::All,
1229 MaxFeatures::Fixed(2),
1230 MaxFeatures::Fraction(0.5),
1231 ] {
1232 let model = RandomForestRegressor::<f64>::new()
1233 .with_n_estimators(5)
1234 .with_max_features(*strategy)
1235 .with_random_state(42);
1236 let fitted = model.fit(&x, &y).unwrap();
1237 let preds = fitted.predict(&x).unwrap();
1238 assert_eq!(preds.len(), 8);
1239 }
1240 }
1241
1242 #[test]
1245 fn test_resolve_max_features_sqrt() {
1246 assert_eq!(resolve_max_features(MaxFeatures::Sqrt, 9), 3);
1247 assert_eq!(resolve_max_features(MaxFeatures::Sqrt, 10), 4);
1248 assert_eq!(resolve_max_features(MaxFeatures::Sqrt, 1), 1);
1249 }
1250
1251 #[test]
1252 fn test_resolve_max_features_log2() {
1253 assert_eq!(resolve_max_features(MaxFeatures::Log2, 8), 3);
1254 assert_eq!(resolve_max_features(MaxFeatures::Log2, 1), 1);
1255 }
1256
1257 #[test]
1258 fn test_resolve_max_features_all() {
1259 assert_eq!(resolve_max_features(MaxFeatures::All, 10), 10);
1260 assert_eq!(resolve_max_features(MaxFeatures::All, 1), 1);
1261 }
1262
1263 #[test]
1264 fn test_resolve_max_features_fixed() {
1265 assert_eq!(resolve_max_features(MaxFeatures::Fixed(3), 10), 3);
1266 assert_eq!(resolve_max_features(MaxFeatures::Fixed(20), 10), 10);
1267 }
1268
1269 #[test]
1270 fn test_resolve_max_features_fraction() {
1271 assert_eq!(resolve_max_features(MaxFeatures::Fraction(0.5), 10), 5);
1272 assert_eq!(resolve_max_features(MaxFeatures::Fraction(0.1), 10), 1);
1273 }
1274
1275 #[test]
1276 fn test_forest_classifier_f32_support() {
1277 let x = Array2::from_shape_vec((6, 1), vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
1278 let y = array![0, 0, 0, 1, 1, 1];
1279
1280 let model = RandomForestClassifier::<f32>::new()
1281 .with_n_estimators(5)
1282 .with_random_state(42);
1283 let fitted = model.fit(&x, &y).unwrap();
1284 let preds = fitted.predict(&x).unwrap();
1285 assert_eq!(preds.len(), 6);
1286 }
1287
1288 #[test]
1289 fn test_forest_regressor_f32_support() {
1290 let x = Array2::from_shape_vec((4, 1), vec![1.0f32, 2.0, 3.0, 4.0]).unwrap();
1291 let y = Array1::from_vec(vec![1.0f32, 2.0, 3.0, 4.0]);
1292
1293 let model = RandomForestRegressor::<f32>::new()
1294 .with_n_estimators(5)
1295 .with_random_state(42);
1296 let fitted = model.fit(&x, &y).unwrap();
1297 let preds = fitted.predict(&x).unwrap();
1298 assert_eq!(preds.len(), 4);
1299 }
1300}