1use scirs2_core::ndarray::{Array2, ArrayView1, ArrayView2};
8use sklears_core::error::{Result as SklResult, SklearsError};
9use std::marker::PhantomData;
10
11type Result<T> = SklResult<T>;
12
13pub mod selection_types {
15 #[derive(Debug, Clone, Copy, Default)]
17 pub struct Filter;
18
19 #[derive(Debug, Clone, Copy, Default)]
21 pub struct Wrapper;
22
23 #[derive(Debug, Clone, Copy, Default)]
25 pub struct Embedded;
26
27 #[derive(Debug, Clone, Copy, Default)]
29 pub struct Univariate;
30
31 #[derive(Debug, Clone, Copy, Default)]
33 pub struct Multivariate;
34
35 #[derive(Debug, Clone, Copy, Default)]
37 pub struct Supervised;
38
39 #[derive(Debug, Clone, Copy, Default)]
41 pub struct Unsupervised;
42
43 #[derive(Debug, Clone, Copy, Default)]
45 pub struct Deterministic;
46
47 #[derive(Debug, Clone, Copy, Default)]
49 pub struct Stochastic;
50}
51
52pub mod data_states {
54 #[derive(Debug, Clone, Copy, Default)]
56 pub struct Untrained;
57
58 #[derive(Debug, Clone, Copy, Default)]
60 pub struct Trained;
61
62 #[derive(Debug, Clone, Copy, Default)]
64 pub struct Validated;
65
66 #[derive(Debug, Clone, Copy, Default)]
68 pub struct Optimized;
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
73pub struct FeatureIndex<const MAX_FEATURES: usize> {
74 index: usize,
75}
76
77impl<const MAX_FEATURES: usize> FeatureIndex<MAX_FEATURES> {
78 pub const fn new(index: usize) -> Option<Self> {
80 if index < MAX_FEATURES {
81 Some(Self { index })
82 } else {
83 None
84 }
85 }
86
87 pub const unsafe fn new_unchecked(index: usize) -> Self {
92 Self { index }
93 }
94
95 pub const fn get(self) -> usize {
97 self.index
98 }
99
100 pub const fn to_runtime(self) -> RuntimeFeatureIndex {
102 RuntimeFeatureIndex::new(self.index)
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
108pub struct RuntimeFeatureIndex {
109 index: usize,
110}
111
112impl RuntimeFeatureIndex {
113 pub const fn new(index: usize) -> Self {
115 Self { index }
116 }
117
118 pub const fn get(self) -> usize {
120 self.index
121 }
122
123 pub const fn is_valid(self, n_features: usize) -> bool {
125 self.index < n_features
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct FeatureMask<const N_FEATURES: usize> {
132 mask: [bool; N_FEATURES],
133}
134
135impl<const N_FEATURES: usize> FeatureMask<N_FEATURES> {
136 pub const fn all_selected() -> Self {
138 Self {
139 mask: [true; N_FEATURES],
140 }
141 }
142
143 pub const fn none_selected() -> Self {
145 Self {
146 mask: [false; N_FEATURES],
147 }
148 }
149
150 pub const fn from_array(mask: [bool; N_FEATURES]) -> Self {
152 Self { mask }
153 }
154
155 pub fn from_indices(indices: &[FeatureIndex<N_FEATURES>]) -> Self {
157 let mut mask = [false; N_FEATURES];
158 for &index in indices {
159 mask[index.get()] = true;
160 }
161 Self { mask }
162 }
163
164 pub const fn as_array(&self) -> &[bool; N_FEATURES] {
166 &self.mask
167 }
168
169 pub const fn is_selected(&self, index: FeatureIndex<N_FEATURES>) -> bool {
171 self.mask[index.get()]
172 }
173
174 pub fn set(&mut self, index: FeatureIndex<N_FEATURES>, selected: bool) {
176 self.mask[index.get()] = selected;
177 }
178
179 pub fn count_selected(&self) -> usize {
181 self.mask.iter().filter(|&&x| x).count()
182 }
183
184 pub fn selected_indices(&self) -> Vec<FeatureIndex<N_FEATURES>> {
186 self.mask
187 .iter()
188 .enumerate()
189 .filter_map(|(i, &selected)| {
190 if selected {
191 Some(unsafe { FeatureIndex::new_unchecked(i) })
193 } else {
194 None
195 }
196 })
197 .collect()
198 }
199
200 pub fn and(&self, other: &Self) -> Self {
202 let mut result = [false; N_FEATURES];
203 for i in 0..N_FEATURES {
204 result[i] = self.mask[i] && other.mask[i];
205 }
206 Self::from_array(result)
207 }
208
209 pub fn or(&self, other: &Self) -> Self {
211 let mut result = [false; N_FEATURES];
212 for i in 0..N_FEATURES {
213 result[i] = self.mask[i] || other.mask[i];
214 }
215 Self::from_array(result)
216 }
217
218 pub fn not(&self) -> Self {
220 let mut result = [false; N_FEATURES];
221 for i in 0..N_FEATURES {
222 result[i] = !self.mask[i];
223 }
224 Self::from_array(result)
225 }
226}
227
228#[derive(Debug, Clone)]
230pub struct FeatureMatrix<T, const N_FEATURES: usize> {
231 data: Array2<T>,
232 _phantom: PhantomData<[T; N_FEATURES]>,
233}
234
235impl<T, const N_FEATURES: usize> FeatureMatrix<T, N_FEATURES>
236where
237 T: Clone + Default,
238{
239 pub fn new(data: Array2<T>) -> Result<Self> {
241 if data.ncols() == N_FEATURES {
242 Ok(Self {
243 data,
244 _phantom: PhantomData,
245 })
246 } else {
247 Err(SklearsError::InvalidInput(format!(
248 "Expected {} features, got {}",
249 N_FEATURES,
250 data.ncols()
251 )))
252 }
253 }
254
255 pub unsafe fn new_unchecked(data: Array2<T>) -> Self {
260 Self {
261 data,
262 _phantom: PhantomData,
263 }
264 }
265
266 pub fn n_samples(&self) -> usize {
268 self.data.nrows()
269 }
270
271 pub const fn n_features(&self) -> usize {
273 N_FEATURES
274 }
275
276 pub fn view(&self) -> ArrayView2<'_, T> {
278 self.data.view()
279 }
280
281 pub fn feature(&self, index: FeatureIndex<N_FEATURES>) -> ArrayView1<'_, T> {
283 self.data.column(index.get())
284 }
285
286 pub fn select_features<const N_SELECTED: usize>(
288 &self,
289 mask: &FeatureMask<N_FEATURES>,
290 ) -> Result<FeatureMatrix<T, N_SELECTED>> {
291 let selected_indices = mask.selected_indices();
292 if selected_indices.len() != N_SELECTED {
293 return Err(SklearsError::InvalidInput(format!(
294 "Expected {} selected features, got {}",
295 N_SELECTED,
296 selected_indices.len()
297 )));
298 }
299
300 let mut selected_data = Array2::default((self.n_samples(), N_SELECTED));
301 for (new_col, &old_index) in selected_indices.iter().enumerate() {
302 for row in 0..self.n_samples() {
303 selected_data[[row, new_col]] = self.data[[row, old_index.get()]].clone();
304 }
305 }
306
307 Ok(FeatureMatrix {
308 data: selected_data,
309 _phantom: PhantomData,
310 })
311 }
312
313 pub fn to_dynamic(self) -> DynamicFeatureMatrix<T> {
315 DynamicFeatureMatrix::new(self.data)
316 }
317}
318
319#[derive(Debug, Clone)]
321pub struct DynamicFeatureMatrix<T> {
322 data: Array2<T>,
323}
324
325impl<T> DynamicFeatureMatrix<T> {
326 pub fn new(data: Array2<T>) -> Self {
328 Self { data }
329 }
330
331 pub fn n_samples(&self) -> usize {
333 self.data.nrows()
334 }
335
336 pub fn n_features(&self) -> usize {
338 self.data.ncols()
339 }
340
341 pub fn view(&self) -> ArrayView2<'_, T> {
343 self.data.view()
344 }
345
346 pub fn feature(&self, index: RuntimeFeatureIndex) -> Result<ArrayView1<'_, T>> {
348 if index.is_valid(self.n_features()) {
349 Ok(self.data.column(index.get()))
350 } else {
351 Err(SklearsError::InvalidInput(format!(
352 "Feature index {} out of bounds for {} features",
353 index.get(),
354 self.n_features()
355 )))
356 }
357 }
358
359 pub fn to_static<const N_FEATURES: usize>(self) -> Result<FeatureMatrix<T, N_FEATURES>>
361 where
362 T: Clone + Default,
363 {
364 FeatureMatrix::new(self.data)
365 }
366}
367
368pub trait TypeSafeSelector<Method, State = data_states::Untrained> {
370 type FittedState;
372
373 type SelectionResult;
375
376 fn fit_typed<const N_FEATURES: usize>(
378 self,
379 X: &FeatureMatrix<f64, N_FEATURES>,
380 y: ArrayView1<f64>,
381 ) -> Result<TypeSafeSelectorWrapper<Method, Self::FittedState, N_FEATURES>>;
382}
383
384#[derive(Debug, Clone)]
386pub struct TypeSafeSelectorWrapper<Method, State, const N_FEATURES: usize> {
387 _method_params: MethodParameters,
388 selection_result: Option<FeatureMask<N_FEATURES>>,
389 _phantom: PhantomData<(Method, State)>,
390}
391
392impl<Method, State, const N_FEATURES: usize> TypeSafeSelectorWrapper<Method, State, N_FEATURES> {
393 pub fn new(method_params: MethodParameters) -> Self {
395 Self {
396 _method_params: method_params,
397 selection_result: None,
398 _phantom: PhantomData,
399 }
400 }
401
402 pub fn selection_mask(&self) -> Option<&FeatureMask<N_FEATURES>> {
404 self.selection_result.as_ref()
405 }
406
407 pub fn set_selection(&mut self, mask: FeatureMask<N_FEATURES>) {
409 self.selection_result = Some(mask);
410 }
411}
412
413#[derive(Debug, Clone)]
415pub enum MethodParameters {
416 VarianceThreshold {
418 threshold: f64,
420 },
421 UnivariateFilter {
423 k: usize,
425
426 score_function: String,
428 },
429 RecursiveElimination {
431 n_features: usize,
433
434 step: f64,
436 },
437 LassoSelection {
439 alpha: f64,
441 max_iter: usize,
443 },
444 TreeBasedSelection {
446 n_estimators: usize,
448 max_depth: Option<usize>,
450 },
451 CorrelationFilter {
453 threshold: f64,
455 },
456 MutualInfoSelection {
458 k: usize,
460 discrete_features: Vec<bool>,
462 },
463}
464
465#[derive(Debug, Clone)]
467pub struct VarianceThresholdSelector<const N_FEATURES: usize> {
468 threshold: f64,
469 feature_variances: Option<[f64; N_FEATURES]>,
470}
471
472impl<const N_FEATURES: usize> VarianceThresholdSelector<N_FEATURES> {
473 pub const fn new(threshold: f64) -> Self {
475 Self {
476 threshold,
477 feature_variances: None,
478 }
479 }
480
481 pub fn fit(&mut self, X: &FeatureMatrix<f64, N_FEATURES>) -> Result<FeatureMask<N_FEATURES>> {
483 let mut variances = [0.0; N_FEATURES];
484
485 for i in 0..N_FEATURES {
486 let feature_index = unsafe { FeatureIndex::new_unchecked(i) };
488 let feature_data = X.feature(feature_index);
489 variances[i] = feature_data.var(1.0);
490 }
491
492 self.feature_variances = Some(variances);
493
494 let mut mask = [false; N_FEATURES];
495 for i in 0..N_FEATURES {
496 mask[i] = variances[i] > self.threshold;
497 }
498
499 Ok(FeatureMask::from_array(mask))
500 }
501
502 pub fn transform<const N_SELECTED: usize>(
504 &self,
505 X: &FeatureMatrix<f64, N_FEATURES>,
506 mask: &FeatureMask<N_FEATURES>,
507 ) -> Result<FeatureMatrix<f64, N_SELECTED>> {
508 X.select_features(mask)
509 }
510
511 pub const fn feature_variances(&self) -> Option<&[f64; N_FEATURES]> {
513 self.feature_variances.as_ref()
514 }
515}
516
517#[derive(Debug, Clone)]
519pub struct UnivariateSelector<const N_FEATURES: usize, const K: usize> {
520 score_function: UnivariateScoreFunction,
521 feature_scores: Option<[f64; N_FEATURES]>,
522}
523
524impl<const N_FEATURES: usize, const K: usize> UnivariateSelector<N_FEATURES, K> {
525 pub const fn new(score_function: UnivariateScoreFunction) -> Option<Self> {
530 if K <= N_FEATURES {
531 Some(Self {
532 score_function,
533 feature_scores: None,
534 })
535 } else {
536 None
537 }
538 }
539
540 pub fn fit(
542 &mut self,
543 X: &FeatureMatrix<f64, N_FEATURES>,
544 y: ArrayView1<f64>,
545 ) -> Result<FeatureMask<N_FEATURES>> {
546 let mut scores = [0.0; N_FEATURES];
547
548 for i in 0..N_FEATURES {
549 let feature_index = unsafe { FeatureIndex::new_unchecked(i) };
551 let feature_data = X.feature(feature_index);
552 scores[i] = match self.score_function {
553 UnivariateScoreFunction::Correlation => self.compute_correlation(feature_data, y),
554 UnivariateScoreFunction::MutualInfo => self.compute_mutual_info(feature_data, y),
555 UnivariateScoreFunction::Chi2 => self.compute_chi2_score(feature_data, y),
556 UnivariateScoreFunction::FStatistic => self.compute_f_statistic(feature_data, y),
557 };
558 }
559
560 self.feature_scores = Some(scores);
561
562 let mut indexed_scores: Vec<(usize, f64)> = Vec::with_capacity(N_FEATURES);
564 for i in 0..N_FEATURES {
565 indexed_scores.push((i, scores[i]));
566 }
567
568 indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
570
571 let mut mask = [false; N_FEATURES];
572 for i in 0..K {
573 if let Some(&(feature_idx, _)) = indexed_scores.get(i) {
574 mask[feature_idx] = true;
575 }
576 }
577
578 Ok(FeatureMask::from_array(mask))
579 }
580
581 pub fn transform(
583 &self,
584 X: &FeatureMatrix<f64, N_FEATURES>,
585 mask: &FeatureMask<N_FEATURES>,
586 ) -> Result<FeatureMatrix<f64, K>> {
587 X.select_features(mask)
588 }
589
590 pub const fn feature_scores(&self) -> Option<&[f64; N_FEATURES]> {
592 self.feature_scores.as_ref()
593 }
594
595 fn compute_correlation(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
597 let n = x.len() as f64;
598 if n < 2.0 {
599 return 0.0;
600 }
601
602 let mean_x = x.mean().unwrap_or(0.0);
603 let mean_y = y.mean().unwrap_or(0.0);
604
605 let mut sum_xy = 0.0;
606 let mut sum_x2 = 0.0;
607 let mut sum_y2 = 0.0;
608
609 for i in 0..x.len() {
610 let dx = x[i] - mean_x;
611 let dy = y[i] - mean_y;
612 sum_xy += dx * dy;
613 sum_x2 += dx * dx;
614 sum_y2 += dy * dy;
615 }
616
617 let denom = (sum_x2 * sum_y2).sqrt();
618 if denom < 1e-10 {
619 0.0
620 } else {
621 (sum_xy / denom).abs()
622 }
623 }
624
625 fn compute_mutual_info(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
626 self.compute_correlation(x, y)
629 }
630
631 fn compute_chi2_score(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
632 self.compute_correlation(x, y)
635 }
636
637 fn compute_f_statistic(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
638 self.compute_correlation(x, y)
641 }
642}
643
644#[derive(Debug, Clone, Copy)]
646pub enum UnivariateScoreFunction {
647 Correlation,
649 MutualInfo,
651 Chi2,
653 FStatistic,
655}
656
657#[derive(Debug, Clone)]
659pub struct CorrelationSelector<const N_FEATURES: usize> {
660 threshold: f64,
661 correlation_matrix: Option<[[f64; N_FEATURES]; N_FEATURES]>,
662}
663
664impl<const N_FEATURES: usize> CorrelationSelector<N_FEATURES> {
665 pub const fn new(threshold: f64) -> Self {
667 Self {
668 threshold,
669 correlation_matrix: None,
670 }
671 }
672
673 pub fn fit(&mut self, X: &FeatureMatrix<f64, N_FEATURES>) -> Result<FeatureMask<N_FEATURES>> {
675 let mut corr_matrix = [[0.0; N_FEATURES]; N_FEATURES];
676
677 for i in 0..N_FEATURES {
679 for j in 0..N_FEATURES {
680 if i == j {
681 corr_matrix[i][j] = 1.0;
682 } else {
683 let feature_i = unsafe { FeatureIndex::new_unchecked(i) };
685 let feature_j = unsafe { FeatureIndex::new_unchecked(j) };
686 let data_i = X.feature(feature_i);
687 let data_j = X.feature(feature_j);
688 corr_matrix[i][j] = self.compute_correlation(data_i, data_j);
689 }
690 }
691 }
692
693 self.correlation_matrix = Some(corr_matrix);
694
695 let mut mask = [true; N_FEATURES];
697 for i in 0..N_FEATURES {
698 for j in (i + 1)..N_FEATURES {
699 if corr_matrix[i][j].abs() > self.threshold && mask[i] && mask[j] {
700 let feature_i = unsafe { FeatureIndex::new_unchecked(i) };
703 let feature_j = unsafe { FeatureIndex::new_unchecked(j) };
704 let var_i = X.feature(feature_i).var(1.0);
705 let var_j = X.feature(feature_j).var(1.0);
706 if var_i < var_j {
707 mask[i] = false;
708 } else {
709 mask[j] = false;
710 }
711 }
712 }
713 }
714
715 Ok(FeatureMask::from_array(mask))
716 }
717
718 pub fn transform<const N_SELECTED: usize>(
720 &self,
721 X: &FeatureMatrix<f64, N_FEATURES>,
722 mask: &FeatureMask<N_FEATURES>,
723 ) -> Result<FeatureMatrix<f64, N_SELECTED>> {
724 X.select_features(mask)
725 }
726
727 pub const fn correlation_matrix(&self) -> Option<&[[f64; N_FEATURES]; N_FEATURES]> {
729 self.correlation_matrix.as_ref()
730 }
731
732 fn compute_correlation(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
733 let n = x.len() as f64;
734 if n < 2.0 {
735 return 0.0;
736 }
737
738 let mean_x = x.mean().unwrap_or(0.0);
739 let mean_y = y.mean().unwrap_or(0.0);
740
741 let mut sum_xy = 0.0;
742 let mut sum_x2 = 0.0;
743 let mut sum_y2 = 0.0;
744
745 for i in 0..x.len() {
746 let dx = x[i] - mean_x;
747 let dy = y[i] - mean_y;
748 sum_xy += dx * dy;
749 sum_x2 += dx * dx;
750 sum_y2 += dy * dy;
751 }
752
753 let denom = (sum_x2 * sum_y2).sqrt();
754 if denom < 1e-10 {
755 0.0
756 } else {
757 sum_xy / denom
758 }
759 }
760}
761
762#[derive(Debug, Clone)]
764pub struct TypeSafeSelectionPipeline<const N_FEATURES: usize, State = data_states::Untrained> {
765 steps: Vec<PipelineStep>,
766 current_mask: Option<FeatureMask<N_FEATURES>>,
767 _phantom: PhantomData<State>,
768}
769
770impl<const N_FEATURES: usize> Default
771 for TypeSafeSelectionPipeline<N_FEATURES, data_states::Untrained>
772{
773 fn default() -> Self {
774 Self::new()
775 }
776}
777
778impl<const N_FEATURES: usize> TypeSafeSelectionPipeline<N_FEATURES, data_states::Untrained> {
779 pub const fn new() -> Self {
781 Self {
782 steps: Vec::new(),
783 current_mask: None,
784 _phantom: PhantomData,
785 }
786 }
787
788 pub fn add_variance_threshold(mut self, threshold: f64) -> Self {
790 self.steps.push(PipelineStep::VarianceThreshold(threshold));
791 self
792 }
793
794 pub fn add_correlation_filter(mut self, threshold: f64) -> Self {
796 self.steps.push(PipelineStep::CorrelationFilter(threshold));
797 self
798 }
799
800 pub fn add_univariate_selection<const K: usize>(
802 mut self,
803 score_function: UnivariateScoreFunction,
804 ) -> Self {
805 self.steps.push(PipelineStep::UnivariateSelection {
806 _k: K,
807 score_function,
808 });
809 self
810 }
811
812 pub fn fit(
814 self,
815 X: &FeatureMatrix<f64, N_FEATURES>,
816 y: ArrayView1<f64>,
817 ) -> Result<TypeSafeSelectionPipeline<N_FEATURES, data_states::Trained>> {
818 let mut current_mask = FeatureMask::all_selected();
819
820 for step in &self.steps {
821 let step_mask = match step {
822 PipelineStep::VarianceThreshold(threshold) => {
823 let mut selector = VarianceThresholdSelector::new(*threshold);
824 selector.fit(X)?
825 }
826 PipelineStep::CorrelationFilter(threshold) => {
827 let mut selector = CorrelationSelector::new(*threshold);
828 selector.fit(X)?
829 }
830 PipelineStep::UnivariateSelection {
831 _k: _,
832 score_function,
833 } => {
834 const DEFAULT_K: usize = 10;
837 if DEFAULT_K <= N_FEATURES {
838 let mut selector =
839 UnivariateSelector::<N_FEATURES, DEFAULT_K>::new(*score_function)
840 .ok_or_else(|| {
841 SklearsError::InvalidInput(
842 "Invalid K for univariate selection".to_string(),
843 )
844 })?;
845 selector.fit(X, y)?
846 } else {
847 FeatureMask::all_selected()
848 }
849 }
850 };
851
852 current_mask = current_mask.and(&step_mask);
853 }
854
855 Ok(TypeSafeSelectionPipeline {
856 steps: self.steps,
857 current_mask: Some(current_mask),
858 _phantom: PhantomData,
859 })
860 }
861}
862
863impl<const N_FEATURES: usize> TypeSafeSelectionPipeline<N_FEATURES, data_states::Trained> {
864 pub fn transform<const N_SELECTED: usize>(
866 &self,
867 X: &FeatureMatrix<f64, N_FEATURES>,
868 ) -> Result<FeatureMatrix<f64, N_SELECTED>> {
869 if let Some(ref mask) = self.current_mask {
870 X.select_features(mask)
871 } else {
872 Err(SklearsError::FitError("Pipeline not fitted".to_string()))
873 }
874 }
875
876 pub fn selection_mask(&self) -> Option<&FeatureMask<N_FEATURES>> {
878 self.current_mask.as_ref()
879 }
880
881 pub fn n_selected_features(&self) -> usize {
883 self.current_mask
884 .as_ref()
885 .map(|mask| mask.count_selected())
886 .unwrap_or(0)
887 }
888}
889
890#[derive(Debug, Clone)]
892enum PipelineStep {
893 VarianceThreshold(f64),
894 CorrelationFilter(f64),
895 UnivariateSelection {
896 _k: usize,
897 score_function: UnivariateScoreFunction,
898 },
899}
900
901pub trait ZeroCostTransform<Input, Output> {
903 fn transform_zero_cost(input: Input) -> Output;
905}
906
907impl<const N: usize> ZeroCostTransform<FeatureIndex<N>, usize> for () {
909 fn transform_zero_cost(input: FeatureIndex<N>) -> usize {
910 input.get()
911 }
912}
913
914impl<const N: usize> ZeroCostTransform<FeatureMask<N>, Vec<bool>> for () {
916 fn transform_zero_cost(input: FeatureMask<N>) -> Vec<bool> {
917 input.as_array().to_vec()
918 }
919}
920
921pub struct FeatureCountValidator<const EXPECTED: usize>;
923
924impl<const EXPECTED: usize> FeatureCountValidator<EXPECTED> {
925 pub const fn validate<const ACTUAL: usize>() -> bool {
927 EXPECTED == ACTUAL
928 }
929
930 pub fn validate_matrix<T>(matrix: FeatureMatrix<T, EXPECTED>) -> FeatureMatrix<T, EXPECTED>
932 where
933 T: Clone + Default,
934 {
935 matrix
936 }
937}
938
939pub trait TypeSafeFeatureSelection {
941 type FeatureMatrix;
943
944 type SelectionResult;
946
947 const INPUT_FEATURES: usize;
949
950 fn select_features_typed(data: Self::FeatureMatrix) -> Result<Self::SelectionResult>;
952}
953
954#[macro_export]
956macro_rules! impl_type_safe_selector {
957 ($selector:ty, $method:ty, $n_features:expr, $n_selected:expr) => {
958 impl TypeSafeFeatureSelection for $selector {
959 type FeatureMatrix = FeatureMatrix<f64, $n_features>;
960 type SelectionResult = FeatureMatrix<f64, $n_selected>;
961 const INPUT_FEATURES: usize = $n_features;
962
963 fn select_features_typed(data: Self::FeatureMatrix) -> Result<Self::SelectionResult> {
964 use $crate::type_safe::VarianceThresholdSelector;
967
968 let mut selector = VarianceThresholdSelector::<$n_features>::new(0.0);
969 let mask = selector.fit(&data)?;
970
971 if mask.count_selected() != $n_selected {
973 return Err(SklearsError::InvalidInput(format!(
974 "Expected {} selected features, got {}. Consider adjusting selection parameters.",
975 $n_selected,
976 mask.count_selected()
977 )));
978 }
979
980 data.select_features(&mask)
981 }
982 }
983 };
984}
985
986pub const fn binomial_coefficient(n: usize, k: usize) -> usize {
988 if k > n {
989 0
990 } else if k == 0 || k == n {
991 1
992 } else {
993 let k = if k > n - k { n - k } else { k };
994 let mut result = 1;
995 let mut i = 0;
996 while i < k {
997 result = result * (n - i) / (i + 1);
998 i += 1;
999 }
1000 result
1001 }
1002}
1003
1004pub const fn validate_selection_count<const N_FEATURES: usize, const K: usize>() -> bool {
1006 K <= N_FEATURES && K > 0
1007}
1008
1009pub trait TypeBool {
1011 const VALUE: bool;
1013}
1014
1015pub struct True;
1017pub struct False;
1019
1020impl TypeBool for True {
1021 const VALUE: bool = true;
1022}
1023
1024impl TypeBool for False {
1025 const VALUE: bool = false;
1026}
1027
1028#[allow(non_snake_case)]
1045#[cfg(test)]
1046mod tests {
1047 use super::*;
1048 use scirs2_core::ndarray::array;
1049
1050 #[test]
1051 fn test_feature_index() {
1052 const MAX_FEATURES: usize = 10;
1053
1054 let valid_index = FeatureIndex::<MAX_FEATURES>::new(5).expect("operation should succeed");
1056 assert_eq!(valid_index.get(), 5);
1057
1058 assert!(FeatureIndex::<MAX_FEATURES>::new(15).is_none());
1060 }
1061
1062 #[test]
1063 fn test_feature_mask() {
1064 const N_FEATURES: usize = 5;
1065
1066 let mask = FeatureMask::<N_FEATURES>::from_array([true, false, true, false, true]);
1067 assert_eq!(mask.count_selected(), 3);
1068
1069 let indices = mask.selected_indices();
1070 assert_eq!(indices.len(), 3);
1071 }
1072
1073 #[test]
1074 fn test_feature_matrix() -> Result<()> {
1075 const N_FEATURES: usize = 3;
1076
1077 let data = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
1078 let matrix = FeatureMatrix::<f64, N_FEATURES>::new(data)?;
1079
1080 assert_eq!(matrix.n_features(), 3);
1081 assert_eq!(matrix.n_samples(), 2);
1082
1083 Ok(())
1084 }
1085
1086 #[test]
1087 fn test_variance_threshold_selector() -> Result<()> {
1088 const N_FEATURES: usize = 3;
1089
1090 let data = array![[1.0, 2.0, 3.0], [1.1, 5.0, 3.1], [0.9, 8.0, 2.9]];
1091 let matrix = FeatureMatrix::<f64, N_FEATURES>::new(data)?;
1092
1093 let mut selector = VarianceThresholdSelector::new(0.1);
1094 let mask = selector.fit(&matrix)?;
1095
1096 assert!(mask.count_selected() > 0);
1098
1099 Ok(())
1100 }
1101
1102 #[test]
1103 fn test_compile_time_validation() {
1104 const N_FEATURES: usize = 10;
1105 const K: usize = 5;
1106
1107 assert!(validate_selection_count::<N_FEATURES, K>());
1109
1110 }
1113
1114 #[test]
1115 fn test_type_safe_pipeline() -> Result<()> {
1116 const N_FEATURES: usize = 4;
1117
1118 let data = array![
1119 [1.0, 2.0, 3.0, 4.0],
1120 [1.1, 5.0, 3.1, 4.1],
1121 [0.9, 8.0, 2.9, 3.9],
1122 [1.2, 2.1, 3.2, 4.2]
1123 ];
1124 let matrix = FeatureMatrix::<f64, N_FEATURES>::new(data)?;
1125 let y = array![0.0, 1.0, 0.0, 1.0];
1126
1127 let pipeline = TypeSafeSelectionPipeline::<N_FEATURES>::new()
1128 .add_variance_threshold(0.01)
1129 .add_correlation_filter(0.9);
1130
1131 let fitted_pipeline = pipeline.fit(&matrix, y.view())?;
1132 assert!(fitted_pipeline.n_selected_features() > 0);
1133
1134 Ok(())
1135 }
1136}