1use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Axis, Dim, OwnedRepr};
4use scirs2_core::random::{rngs::StdRng, thread_rng, RngExt, SeedableRng};
5use sklears_core::{
6 error::{validate, Result as SklResult, SklearsError},
7 traits::{Estimator, Fit, Trained, Transform, Untrained},
8 types::Float,
9};
10use std::marker::PhantomData;
11
12use crate::base::SelectorMixin;
13
14type Mat2f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 2]>, f64>;
19type Vec1f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 1]>, f64>;
20type Vec1i32 = ArrayBase<OwnedRepr<i32>, Dim<[usize; 1]>, i32>;
21type Vec1usize = ArrayBase<OwnedRepr<usize>, Dim<[usize; 1]>, usize>;
22
23#[derive(Debug, Clone)]
26pub struct TreeSelector<E, State = Untrained> {
27 estimator: E,
28 threshold: Option<f64>,
29 max_features: Option<usize>,
30 state: PhantomData<State>,
31 _estimator_: Option<E>,
33 selected_features_: Option<Vec<usize>>,
34 n_features_: Option<usize>,
35 threshold_: Option<f64>,
36}
37
38pub trait TreeImportance {
40 fn feature_importances(&self) -> SklResult<Array1<Float>>;
43}
44
45impl<E: Clone> TreeSelector<E, Untrained> {
46 pub fn new(estimator: E) -> Self {
48 Self {
49 estimator,
50 threshold: None,
51 max_features: None,
52 state: PhantomData,
53 _estimator_: None,
54 selected_features_: None,
55 n_features_: None,
56 threshold_: None,
57 }
58 }
59
60 pub fn threshold(mut self, threshold: f64) -> Self {
63 self.threshold = Some(threshold);
64 self
65 }
66
67 pub fn max_features(mut self, max_features: usize) -> Self {
69 self.max_features = Some(max_features);
70 self
71 }
72}
73
74impl<E> Estimator for TreeSelector<E, Untrained> {
75 type Config = ();
76 type Error = SklearsError;
77 type Float = f64;
78
79 fn config(&self) -> &Self::Config {
80 &()
81 }
82}
83
84macro_rules! impl_fit_for_tree_selector {
85 ($y_type:ty) => {
86 impl<E> Fit<Mat2f64, $y_type> for TreeSelector<E, Untrained>
87 where
88 E: Clone + Fit<Mat2f64, $y_type> + Send + Sync,
89 E::Fitted: TreeImportance + Send + Sync,
90 {
91 type Fitted = TreeSelector<E, Trained>;
92
93 fn fit(self, x: &Mat2f64, y: &$y_type) -> SklResult<Self::Fitted> {
94 let n_features = x.ncols();
95
96 let fitted_estimator = self.estimator.clone().fit(x, y)?;
98
99 let importances = fitted_estimator.feature_importances()?;
101
102 let threshold = if let Some(thresh) = self.threshold {
104 thresh
105 } else {
106 let mut sorted_importances = importances.to_vec();
108 sorted_importances.sort_by(|a, b| {
109 a.partial_cmp(b)
110 .unwrap_or_else(|| match (a.is_nan(), b.is_nan()) {
111 (true, false) => std::cmp::Ordering::Less,
112 (false, true) => std::cmp::Ordering::Greater,
113 _ => std::cmp::Ordering::Equal,
114 })
115 });
116 sorted_importances[sorted_importances.len() / 2]
117 };
118
119 let mut selected_features: Vec<usize> = (0..n_features)
121 .filter(|&i| importances[i] > threshold)
122 .collect();
123
124 if let Some(max_feat) = self.max_features {
126 if selected_features.len() > max_feat {
127 selected_features.sort_by(|&a, &b| {
129 importances[b]
130 .partial_cmp(&importances[a])
131 .unwrap_or_else(|| {
132 match (importances[b].is_nan(), importances[a].is_nan()) {
133 (true, false) => std::cmp::Ordering::Less,
134 (false, true) => std::cmp::Ordering::Greater,
135 _ => std::cmp::Ordering::Equal,
136 }
137 })
138 });
139 selected_features.truncate(max_feat);
140 selected_features.sort(); }
142 }
143
144 if selected_features.is_empty() {
145 return Err(SklearsError::InvalidInput(format!(
146 "No features selected with threshold={}. Try reducing threshold.",
147 threshold
148 )));
149 }
150
151 Ok(TreeSelector {
152 estimator: self.estimator.clone(),
153 threshold: self.threshold,
154 max_features: self.max_features,
155 state: PhantomData,
156 _estimator_: Some(self.estimator),
157 selected_features_: Some(selected_features),
158 n_features_: Some(n_features),
159 threshold_: Some(threshold),
160 })
161 }
162 }
163 };
164}
165
166impl_fit_for_tree_selector!(Vec1f64);
167impl_fit_for_tree_selector!(Vec1i32);
168impl_fit_for_tree_selector!(Vec1usize);
169
170impl<E> Transform<Array2<Float>> for TreeSelector<E, Trained> {
171 fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
172 let n_features = self.n_features_.ok_or_else(|| {
173 SklearsError::FitError("TreeSelector not fitted: n_features_ missing".to_string())
174 })?;
175 validate::check_n_features(x, n_features)?;
176
177 let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
178 SklearsError::FitError(
179 "TreeSelector not fitted: selected_features_ missing".to_string(),
180 )
181 })?;
182 let n_samples = x.nrows();
183 let k = selected_features.len();
184 let mut x_new = Array2::zeros((n_samples, k));
185
186 for (new_idx, &old_idx) in selected_features.iter().enumerate() {
187 x_new.column_mut(new_idx).assign(&x.column(old_idx));
188 }
189
190 Ok(x_new)
191 }
192}
193
194impl<E> SelectorMixin for TreeSelector<E, Trained> {
195 fn get_support(&self) -> SklResult<Array1<bool>> {
196 let n_features = self.n_features_.ok_or_else(|| {
197 SklearsError::FitError("TreeSelector not fitted: n_features_ missing".to_string())
198 })?;
199 let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
200 SklearsError::FitError(
201 "TreeSelector not fitted: selected_features_ missing".to_string(),
202 )
203 })?;
204 let mut support = Array1::from_elem(n_features, false);
205
206 for &idx in selected_features {
207 support[idx] = true;
208 }
209
210 Ok(support)
211 }
212
213 fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
214 let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
215 SklearsError::FitError(
216 "TreeSelector not fitted: selected_features_ missing".to_string(),
217 )
218 })?;
219 Ok(indices
220 .iter()
221 .filter_map(|&idx| selected_features.iter().position(|&f| f == idx))
222 .collect())
223 }
224}
225
226impl<E> TreeSelector<E, Trained> {
227 pub fn get_support(&self) -> SklResult<Array1<bool>> {
229 let n_features = self.n_features_.ok_or_else(|| {
230 SklearsError::FitError("TreeSelector not fitted: n_features_ missing".to_string())
231 })?;
232 let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
233 SklearsError::FitError(
234 "TreeSelector not fitted: selected_features_ missing".to_string(),
235 )
236 })?;
237 let mut support = Array1::from_elem(n_features, false);
238
239 for &idx in selected_features {
240 support[idx] = true;
241 }
242
243 Ok(support)
244 }
245
246 pub fn threshold(&self) -> SklResult<f64> {
248 self.threshold_.ok_or_else(|| {
249 SklearsError::FitError("TreeSelector not fitted: threshold_ missing".to_string())
250 })
251 }
252
253 pub fn selected_features(&self) -> SklResult<&[usize]> {
255 self.selected_features_.as_deref().ok_or_else(|| {
256 SklearsError::FitError(
257 "TreeSelector not fitted: selected_features_ missing".to_string(),
258 )
259 })
260 }
261}
262
263#[derive(Debug, Clone)]
266pub struct GradientBoostingSelector<State = Untrained> {
267 n_estimators: usize,
268 learning_rate: f64,
269 max_depth: usize,
270 min_samples_split: usize,
271 min_samples_leaf: usize,
272 subsample: f64,
273 threshold: Option<f64>,
274 max_features: Option<usize>,
275 random_state: Option<u64>,
276 state: PhantomData<State>,
277 feature_importances_: Option<Array1<Float>>,
279 selected_features_: Option<Vec<usize>>,
280 n_features_: Option<usize>,
281 _estimators_: Option<Vec<DecisionStump>>,
282}
283
284#[derive(Debug, Clone)]
286pub struct DecisionStump {
287 feature_idx: usize,
288 threshold: f64,
289 left_value: f64,
290 right_value: f64,
291 importance: f64,
292}
293
294impl DecisionStump {
295 fn predict(&self, x: &Array1<Float>) -> f64 {
296 if x[self.feature_idx] <= self.threshold {
297 self.left_value
298 } else {
299 self.right_value
300 }
301 }
302}
303
304impl GradientBoostingSelector<Untrained> {
305 pub fn new() -> Self {
307 Self {
308 n_estimators: 100,
309 learning_rate: 0.1,
310 max_depth: 3,
311 min_samples_split: 2,
312 min_samples_leaf: 1,
313 subsample: 1.0,
314 threshold: None,
315 max_features: None,
316 random_state: None,
317 state: PhantomData,
318 feature_importances_: None,
319 selected_features_: None,
320 n_features_: None,
321 _estimators_: None,
322 }
323 }
324
325 pub fn n_estimators(mut self, n_estimators: usize) -> Result<Self, SklearsError> {
327 if n_estimators < 1 {
328 return Err(SklearsError::InvalidInput(
329 "n_estimators must be at least 1".to_string(),
330 ));
331 }
332 self.n_estimators = n_estimators;
333 Ok(self)
334 }
335
336 pub fn learning_rate(mut self, learning_rate: f64) -> Result<Self, SklearsError> {
338 if learning_rate <= 0.0 {
339 return Err(SklearsError::InvalidInput(
340 "learning_rate must be positive".to_string(),
341 ));
342 }
343 self.learning_rate = learning_rate;
344 Ok(self)
345 }
346
347 pub fn max_depth(mut self, max_depth: usize) -> Result<Self, SklearsError> {
349 if max_depth < 1 {
350 return Err(SklearsError::InvalidInput(
351 "max_depth must be at least 1".to_string(),
352 ));
353 }
354 self.max_depth = max_depth;
355 Ok(self)
356 }
357
358 pub fn min_samples_split(mut self, min_samples_split: usize) -> Result<Self, SklearsError> {
360 if min_samples_split < 2 {
361 return Err(SklearsError::InvalidInput(
362 "min_samples_split must be at least 2".to_string(),
363 ));
364 }
365 self.min_samples_split = min_samples_split;
366 Ok(self)
367 }
368
369 pub fn min_samples_leaf(mut self, min_samples_leaf: usize) -> Result<Self, SklearsError> {
371 if min_samples_leaf < 1 {
372 return Err(SklearsError::InvalidInput(
373 "min_samples_leaf must be at least 1".to_string(),
374 ));
375 }
376 self.min_samples_leaf = min_samples_leaf;
377 Ok(self)
378 }
379
380 pub fn subsample(mut self, subsample: f64) -> Result<Self, SklearsError> {
382 if !(0.0..=1.0).contains(&subsample) {
383 return Err(SklearsError::InvalidInput(
384 "subsample must be between 0 and 1".to_string(),
385 ));
386 }
387 self.subsample = subsample;
388 Ok(self)
389 }
390
391 pub fn threshold(mut self, threshold: Option<f64>) -> Self {
393 self.threshold = threshold;
394 self
395 }
396
397 pub fn max_features(mut self, max_features: Option<usize>) -> Self {
399 self.max_features = max_features;
400 self
401 }
402
403 pub fn random_state(mut self, seed: u64) -> Self {
405 self.random_state = Some(seed);
406 self
407 }
408}
409
410impl Default for GradientBoostingSelector<Untrained> {
411 fn default() -> Self {
412 Self::new()
413 }
414}
415
416impl Estimator for GradientBoostingSelector<Untrained> {
417 type Config = ();
418 type Error = SklearsError;
419 type Float = f64;
420
421 fn config(&self) -> &Self::Config {
422 &()
423 }
424}
425
426impl Fit<Array2<Float>, Array1<Float>> for GradientBoostingSelector<Untrained> {
427 type Fitted = GradientBoostingSelector<Trained>;
428
429 fn fit(self, x: &Array2<Float>, y: &Array1<Float>) -> SklResult<Self::Fitted> {
430 validate::check_consistent_length(x, y)?;
431
432 let (n_samples, n_features) = x.dim();
433 if n_samples < 2 {
434 return Err(SklearsError::InvalidInput(
435 "Gradient Boosting requires at least 2 samples".to_string(),
436 ));
437 }
438
439 let mut rng = if let Some(seed) = self.random_state {
441 StdRng::seed_from_u64(seed)
442 } else {
443 StdRng::from_rng(&mut thread_rng())
444 };
445
446 let mut feature_importances = Array1::zeros(n_features);
448 let mut estimators = Vec::new();
449
450 let y_mean = y.mean().unwrap_or(0.0);
452 let mut predictions = Array1::from_elem(n_samples, y_mean);
453
454 for _ in 0..self.n_estimators {
456 let residuals: Array1<Float> = y - &predictions;
458
459 let (x_sample, residuals_sample) = if self.subsample < 1.0 {
461 let sample_size = (n_samples as f64 * self.subsample).round() as usize;
462 let indices: Vec<usize> = (0..n_samples).collect();
463 let mut sampled_indices = indices;
464 for i in 0..sample_size {
465 let j = rng.random_range(i..n_samples);
466 sampled_indices.swap(i, j);
467 }
468 sampled_indices.truncate(sample_size);
469
470 let x_sub = x.select(Axis(0), &sampled_indices);
471 let r_sub = residuals.select(Axis(0), &sampled_indices);
472 (x_sub, r_sub)
473 } else {
474 (x.clone(), residuals)
475 };
476
477 let mut best_stump = None;
479 let mut best_improvement = f64::NEG_INFINITY;
480
481 for feature_idx in 0..n_features {
482 let mut feature_values: Vec<f64> = x_sample.column(feature_idx).to_vec();
484 feature_values.sort_by(|a, b| {
485 a.partial_cmp(b)
486 .unwrap_or_else(|| match (a.is_nan(), b.is_nan()) {
487 (true, false) => std::cmp::Ordering::Less,
488 (false, true) => std::cmp::Ordering::Greater,
489 _ => std::cmp::Ordering::Equal,
490 })
491 });
492 feature_values.dedup();
493
494 for i in 0..(feature_values.len().saturating_sub(1)) {
496 let threshold = (feature_values[i] + feature_values[i + 1]) / 2.0;
497
498 let (left_indices, right_indices): (Vec<_>, Vec<_>) = (0..x_sample.nrows())
500 .partition(|&j| x_sample[[j, feature_idx]] <= threshold);
501
502 if left_indices.len() < self.min_samples_leaf
503 || right_indices.len() < self.min_samples_leaf
504 {
505 continue;
506 }
507
508 let left_value = if !left_indices.is_empty() {
510 left_indices
511 .iter()
512 .map(|&idx| residuals_sample[idx])
513 .sum::<f64>()
514 / left_indices.len() as f64
515 } else {
516 0.0
517 };
518
519 let right_value = if !right_indices.is_empty() {
520 right_indices
521 .iter()
522 .map(|&idx| residuals_sample[idx])
523 .sum::<f64>()
524 / right_indices.len() as f64
525 } else {
526 0.0
527 };
528
529 let total_var = residuals_sample.mapv(|x| x * x).sum();
531 let left_var: f64 = left_indices
532 .iter()
533 .map(|&idx| (residuals_sample[idx] - left_value).powi(2))
534 .sum();
535 let right_var: f64 = right_indices
536 .iter()
537 .map(|&idx| (residuals_sample[idx] - right_value).powi(2))
538 .sum();
539
540 let improvement = total_var - left_var - right_var;
541
542 if improvement > best_improvement {
543 best_improvement = improvement;
544 best_stump = Some(DecisionStump {
545 feature_idx,
546 threshold,
547 left_value,
548 right_value,
549 importance: improvement,
550 });
551 }
552 }
553 }
554
555 if let Some(stump) = best_stump {
556 feature_importances[stump.feature_idx] += stump.importance;
558
559 for i in 0..n_samples {
561 let row = x.row(i);
562 let prediction = stump.predict(&row.to_owned());
563 predictions[i] += self.learning_rate * prediction;
564 }
565
566 estimators.push(stump);
567 }
568 }
569
570 let total_importance = feature_importances.sum();
572 if total_importance > 0.0 {
573 feature_importances /= total_importance;
574 }
575
576 let threshold = self
578 .threshold
579 .unwrap_or_else(|| feature_importances.mean().unwrap_or(0.0));
580
581 let mut selected_features: Vec<usize> = feature_importances
582 .iter()
583 .enumerate()
584 .filter(|(_, &importance)| importance > threshold)
585 .map(|(idx, _)| idx)
586 .collect();
587
588 if let Some(max_feat) = self.max_features {
590 if selected_features.len() > max_feat {
591 selected_features.sort_by(|&a, &b| {
593 feature_importances[b]
594 .partial_cmp(&feature_importances[a])
595 .unwrap_or_else(|| {
596 match (
597 feature_importances[b].is_nan(),
598 feature_importances[a].is_nan(),
599 ) {
600 (true, false) => std::cmp::Ordering::Less,
601 (false, true) => std::cmp::Ordering::Greater,
602 _ => std::cmp::Ordering::Equal,
603 }
604 })
605 });
606 selected_features.truncate(max_feat);
607 selected_features.sort(); }
609 }
610
611 if selected_features.is_empty() {
612 return Err(SklearsError::InvalidInput(
613 "No features selected by Gradient Boosting. Try reducing threshold.".to_string(),
614 ));
615 }
616
617 Ok(GradientBoostingSelector {
618 n_estimators: self.n_estimators,
619 learning_rate: self.learning_rate,
620 max_depth: self.max_depth,
621 min_samples_split: self.min_samples_split,
622 min_samples_leaf: self.min_samples_leaf,
623 subsample: self.subsample,
624 threshold: self.threshold,
625 max_features: self.max_features,
626 random_state: self.random_state,
627 state: PhantomData,
628 feature_importances_: Some(feature_importances),
629 selected_features_: Some(selected_features),
630 n_features_: Some(n_features),
631 _estimators_: Some(estimators),
632 })
633 }
634}
635
636impl Transform<Array2<Float>> for GradientBoostingSelector<Trained> {
637 fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
638 let n_features = self.n_features_.ok_or_else(|| {
639 SklearsError::FitError(
640 "GradientBoostingSelector not fitted: n_features_ missing".to_string(),
641 )
642 })?;
643 validate::check_n_features(x, n_features)?;
644
645 let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
646 SklearsError::FitError(
647 "GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
648 )
649 })?;
650 extract_features(x, selected_features)
651 }
652}
653
654impl SelectorMixin for GradientBoostingSelector<Trained> {
655 fn get_support(&self) -> SklResult<Array1<bool>> {
656 let n_features = self.n_features_.ok_or_else(|| {
657 SklearsError::FitError(
658 "GradientBoostingSelector not fitted: n_features_ missing".to_string(),
659 )
660 })?;
661 let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
662 SklearsError::FitError(
663 "GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
664 )
665 })?;
666 let mut support = Array1::from_elem(n_features, false);
667
668 for &idx in selected_features {
669 support[idx] = true;
670 }
671
672 Ok(support)
673 }
674
675 fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
676 let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
677 SklearsError::FitError(
678 "GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
679 )
680 })?;
681 Ok(indices
682 .iter()
683 .filter_map(|&idx| selected_features.iter().position(|&f| f == idx))
684 .collect())
685 }
686}
687
688impl GradientBoostingSelector<Trained> {
689 pub fn feature_importances(&self) -> SklResult<&Array1<Float>> {
691 self.feature_importances_.as_ref().ok_or_else(|| {
692 SklearsError::FitError(
693 "GradientBoostingSelector not fitted: feature_importances_ missing".to_string(),
694 )
695 })
696 }
697
698 pub fn selected_features(&self) -> SklResult<&[usize]> {
700 self.selected_features_.as_deref().ok_or_else(|| {
701 SklearsError::FitError(
702 "GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
703 )
704 })
705 }
706
707 pub fn estimators(&self) -> SklResult<&[DecisionStump]> {
709 self._estimators_.as_deref().ok_or_else(|| {
710 SklearsError::FitError(
711 "GradientBoostingSelector not fitted: estimators_ missing".to_string(),
712 )
713 })
714 }
715}
716
717#[derive(Debug, Clone)]
719pub struct TreeImportanceSelector {
720 n_estimators: usize,
721 max_depth: Option<usize>,
722 random_state: Option<u64>,
723}
724
725impl Default for TreeImportanceSelector {
726 fn default() -> Self {
727 Self::new()
728 }
729}
730
731impl TreeImportanceSelector {
732 pub fn new() -> Self {
734 Self {
735 n_estimators: 100,
736 max_depth: None,
737 random_state: None,
738 }
739 }
740
741 pub fn n_estimators(mut self, n_estimators: usize) -> Self {
743 self.n_estimators = n_estimators;
744 self
745 }
746
747 pub fn max_depth(mut self, max_depth: Option<usize>) -> Self {
749 self.max_depth = max_depth;
750 self
751 }
752
753 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
755 self.random_state = random_state;
756 self
757 }
758}
759
760impl crate::ensemble_selectors::SelectorFunction for TreeImportanceSelector {
761 fn compute_scores(
762 &self,
763 x: &Array2<f64>,
764 y_classif: Option<&Array1<i32>>,
765 y_regression: Option<&Array1<f64>>,
766 ) -> SklResult<Array1<f64>> {
767 let n_features = x.ncols();
768 let n_samples = x.nrows();
769
770 let mut rng = if let Some(seed) = self.random_state {
773 StdRng::seed_from_u64(seed)
774 } else {
775 StdRng::from_rng(&mut thread_rng())
776 };
777
778 let mut feature_importance = Array1::<f64>::zeros(n_features);
779
780 if let Some(y) = y_classif {
782 for i in 0..n_features {
784 let feature_col = x.column(i);
785 let mut score = 0.0;
786
787 for j in 0..n_samples {
789 let feature_val = feature_col[j];
790 let target_val = y[j] as f64;
791 score += (feature_val * target_val).abs();
792 }
793
794 feature_importance[i] = score / n_samples as f64;
795 }
796 } else if let Some(y) = y_regression {
797 for i in 0..n_features {
799 let feature_col = x.column(i);
800 let correlation = compute_pearson_correlation(&feature_col.to_owned(), y);
801 feature_importance[i] = correlation.abs();
802 }
803 } else {
804 return Err(SklearsError::InvalidInput(
805 "Need either classification or regression target".to_string(),
806 ));
807 }
808
809 for importance in feature_importance.iter_mut() {
811 *importance += rng.random::<f64>() * 0.1; }
813
814 Ok(feature_importance)
815 }
816
817 fn name(&self) -> &str {
818 "tree_importance"
819 }
820
821 fn clone_box(&self) -> Box<dyn crate::ensemble_selectors::SelectorFunction> {
822 Box::new(self.clone())
823 }
824}
825
826fn compute_pearson_correlation(x: &Array1<f64>, y: &Array1<f64>) -> f64 {
828 let n = x.len() as f64;
829 if n < 2.0 {
830 return 0.0;
831 }
832
833 let mean_x = x.mean().unwrap_or(0.0);
834 let mean_y = y.mean().unwrap_or(0.0);
835
836 let mut numerator = 0.0;
837 let mut sum_sq_x = 0.0;
838 let mut sum_sq_y = 0.0;
839
840 for (&xi, &yi) in x.iter().zip(y.iter()) {
841 let dx = xi - mean_x;
842 let dy = yi - mean_y;
843 numerator += dx * dy;
844 sum_sq_x += dx * dx;
845 sum_sq_y += dy * dy;
846 }
847
848 let denominator = (sum_sq_x * sum_sq_y).sqrt();
849 if denominator == 0.0 {
850 0.0
851 } else {
852 numerator / denominator
853 }
854}
855
856fn extract_features(x: &Array2<Float>, features: &[usize]) -> SklResult<Array2<Float>> {
858 let n_samples = x.nrows();
859 let n_selected = features.len();
860 let mut x_subset = Array2::zeros((n_samples, n_selected));
861
862 for (new_idx, &old_idx) in features.iter().enumerate() {
863 if old_idx >= x.ncols() {
864 return Err(SklearsError::InvalidInput(format!(
865 "Feature index {} out of bounds",
866 old_idx
867 )));
868 }
869 x_subset.column_mut(new_idx).assign(&x.column(old_idx));
870 }
871
872 Ok(x_subset)
873}