use scirs2_core::ndarray::{Array2, ArrayView1, ArrayView2};
use sklears_core::error::{Result as SklResult, SklearsError};
use std::marker::PhantomData;
type Result<T> = SklResult<T>;
pub mod selection_types {
#[derive(Debug, Clone, Copy, Default)]
pub struct Filter;
#[derive(Debug, Clone, Copy, Default)]
pub struct Wrapper;
#[derive(Debug, Clone, Copy, Default)]
pub struct Embedded;
#[derive(Debug, Clone, Copy, Default)]
pub struct Univariate;
#[derive(Debug, Clone, Copy, Default)]
pub struct Multivariate;
#[derive(Debug, Clone, Copy, Default)]
pub struct Supervised;
#[derive(Debug, Clone, Copy, Default)]
pub struct Unsupervised;
#[derive(Debug, Clone, Copy, Default)]
pub struct Deterministic;
#[derive(Debug, Clone, Copy, Default)]
pub struct Stochastic;
}
pub mod data_states {
#[derive(Debug, Clone, Copy, Default)]
pub struct Untrained;
#[derive(Debug, Clone, Copy, Default)]
pub struct Trained;
#[derive(Debug, Clone, Copy, Default)]
pub struct Validated;
#[derive(Debug, Clone, Copy, Default)]
pub struct Optimized;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct FeatureIndex<const MAX_FEATURES: usize> {
index: usize,
}
impl<const MAX_FEATURES: usize> FeatureIndex<MAX_FEATURES> {
pub const fn new(index: usize) -> Option<Self> {
if index < MAX_FEATURES {
Some(Self { index })
} else {
None
}
}
pub const unsafe fn new_unchecked(index: usize) -> Self {
Self { index }
}
pub const fn get(self) -> usize {
self.index
}
pub const fn to_runtime(self) -> RuntimeFeatureIndex {
RuntimeFeatureIndex::new(self.index)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct RuntimeFeatureIndex {
index: usize,
}
impl RuntimeFeatureIndex {
pub const fn new(index: usize) -> Self {
Self { index }
}
pub const fn get(self) -> usize {
self.index
}
pub const fn is_valid(self, n_features: usize) -> bool {
self.index < n_features
}
}
#[derive(Debug, Clone)]
pub struct FeatureMask<const N_FEATURES: usize> {
mask: [bool; N_FEATURES],
}
impl<const N_FEATURES: usize> FeatureMask<N_FEATURES> {
pub const fn all_selected() -> Self {
Self {
mask: [true; N_FEATURES],
}
}
pub const fn none_selected() -> Self {
Self {
mask: [false; N_FEATURES],
}
}
pub const fn from_array(mask: [bool; N_FEATURES]) -> Self {
Self { mask }
}
pub fn from_indices(indices: &[FeatureIndex<N_FEATURES>]) -> Self {
let mut mask = [false; N_FEATURES];
for &index in indices {
mask[index.get()] = true;
}
Self { mask }
}
pub const fn as_array(&self) -> &[bool; N_FEATURES] {
&self.mask
}
pub const fn is_selected(&self, index: FeatureIndex<N_FEATURES>) -> bool {
self.mask[index.get()]
}
pub fn set(&mut self, index: FeatureIndex<N_FEATURES>, selected: bool) {
self.mask[index.get()] = selected;
}
pub fn count_selected(&self) -> usize {
self.mask.iter().filter(|&&x| x).count()
}
pub fn selected_indices(&self) -> Vec<FeatureIndex<N_FEATURES>> {
self.mask
.iter()
.enumerate()
.filter_map(|(i, &selected)| {
if selected {
Some(unsafe { FeatureIndex::new_unchecked(i) })
} else {
None
}
})
.collect()
}
pub fn and(&self, other: &Self) -> Self {
let mut result = [false; N_FEATURES];
for i in 0..N_FEATURES {
result[i] = self.mask[i] && other.mask[i];
}
Self::from_array(result)
}
pub fn or(&self, other: &Self) -> Self {
let mut result = [false; N_FEATURES];
for i in 0..N_FEATURES {
result[i] = self.mask[i] || other.mask[i];
}
Self::from_array(result)
}
pub fn not(&self) -> Self {
let mut result = [false; N_FEATURES];
for i in 0..N_FEATURES {
result[i] = !self.mask[i];
}
Self::from_array(result)
}
}
#[derive(Debug, Clone)]
pub struct FeatureMatrix<T, const N_FEATURES: usize> {
data: Array2<T>,
_phantom: PhantomData<[T; N_FEATURES]>,
}
impl<T, const N_FEATURES: usize> FeatureMatrix<T, N_FEATURES>
where
T: Clone + Default,
{
pub fn new(data: Array2<T>) -> Result<Self> {
if data.ncols() == N_FEATURES {
Ok(Self {
data,
_phantom: PhantomData,
})
} else {
Err(SklearsError::InvalidInput(format!(
"Expected {} features, got {}",
N_FEATURES,
data.ncols()
)))
}
}
pub unsafe fn new_unchecked(data: Array2<T>) -> Self {
Self {
data,
_phantom: PhantomData,
}
}
pub fn n_samples(&self) -> usize {
self.data.nrows()
}
pub const fn n_features(&self) -> usize {
N_FEATURES
}
pub fn view(&self) -> ArrayView2<'_, T> {
self.data.view()
}
pub fn feature(&self, index: FeatureIndex<N_FEATURES>) -> ArrayView1<'_, T> {
self.data.column(index.get())
}
pub fn select_features<const N_SELECTED: usize>(
&self,
mask: &FeatureMask<N_FEATURES>,
) -> Result<FeatureMatrix<T, N_SELECTED>> {
let selected_indices = mask.selected_indices();
if selected_indices.len() != N_SELECTED {
return Err(SklearsError::InvalidInput(format!(
"Expected {} selected features, got {}",
N_SELECTED,
selected_indices.len()
)));
}
let mut selected_data = Array2::default((self.n_samples(), N_SELECTED));
for (new_col, &old_index) in selected_indices.iter().enumerate() {
for row in 0..self.n_samples() {
selected_data[[row, new_col]] = self.data[[row, old_index.get()]].clone();
}
}
Ok(FeatureMatrix {
data: selected_data,
_phantom: PhantomData,
})
}
pub fn to_dynamic(self) -> DynamicFeatureMatrix<T> {
DynamicFeatureMatrix::new(self.data)
}
}
#[derive(Debug, Clone)]
pub struct DynamicFeatureMatrix<T> {
data: Array2<T>,
}
impl<T> DynamicFeatureMatrix<T> {
pub fn new(data: Array2<T>) -> Self {
Self { data }
}
pub fn n_samples(&self) -> usize {
self.data.nrows()
}
pub fn n_features(&self) -> usize {
self.data.ncols()
}
pub fn view(&self) -> ArrayView2<'_, T> {
self.data.view()
}
pub fn feature(&self, index: RuntimeFeatureIndex) -> Result<ArrayView1<'_, T>> {
if index.is_valid(self.n_features()) {
Ok(self.data.column(index.get()))
} else {
Err(SklearsError::InvalidInput(format!(
"Feature index {} out of bounds for {} features",
index.get(),
self.n_features()
)))
}
}
pub fn to_static<const N_FEATURES: usize>(self) -> Result<FeatureMatrix<T, N_FEATURES>>
where
T: Clone + Default,
{
FeatureMatrix::new(self.data)
}
}
pub trait TypeSafeSelector<Method, State = data_states::Untrained> {
type FittedState;
type SelectionResult;
fn fit_typed<const N_FEATURES: usize>(
self,
X: &FeatureMatrix<f64, N_FEATURES>,
y: ArrayView1<f64>,
) -> Result<TypeSafeSelectorWrapper<Method, Self::FittedState, N_FEATURES>>;
}
#[derive(Debug, Clone)]
pub struct TypeSafeSelectorWrapper<Method, State, const N_FEATURES: usize> {
_method_params: MethodParameters,
selection_result: Option<FeatureMask<N_FEATURES>>,
_phantom: PhantomData<(Method, State)>,
}
impl<Method, State, const N_FEATURES: usize> TypeSafeSelectorWrapper<Method, State, N_FEATURES> {
pub fn new(method_params: MethodParameters) -> Self {
Self {
_method_params: method_params,
selection_result: None,
_phantom: PhantomData,
}
}
pub fn selection_mask(&self) -> Option<&FeatureMask<N_FEATURES>> {
self.selection_result.as_ref()
}
pub fn set_selection(&mut self, mask: FeatureMask<N_FEATURES>) {
self.selection_result = Some(mask);
}
}
#[derive(Debug, Clone)]
pub enum MethodParameters {
VarianceThreshold {
threshold: f64,
},
UnivariateFilter {
k: usize,
score_function: String,
},
RecursiveElimination {
n_features: usize,
step: f64,
},
LassoSelection {
alpha: f64,
max_iter: usize,
},
TreeBasedSelection {
n_estimators: usize,
max_depth: Option<usize>,
},
CorrelationFilter {
threshold: f64,
},
MutualInfoSelection {
k: usize,
discrete_features: Vec<bool>,
},
}
#[derive(Debug, Clone)]
pub struct VarianceThresholdSelector<const N_FEATURES: usize> {
threshold: f64,
feature_variances: Option<[f64; N_FEATURES]>,
}
impl<const N_FEATURES: usize> VarianceThresholdSelector<N_FEATURES> {
pub const fn new(threshold: f64) -> Self {
Self {
threshold,
feature_variances: None,
}
}
pub fn fit(&mut self, X: &FeatureMatrix<f64, N_FEATURES>) -> Result<FeatureMask<N_FEATURES>> {
let mut variances = [0.0; N_FEATURES];
for i in 0..N_FEATURES {
let feature_index = unsafe { FeatureIndex::new_unchecked(i) };
let feature_data = X.feature(feature_index);
variances[i] = feature_data.var(1.0);
}
self.feature_variances = Some(variances);
let mut mask = [false; N_FEATURES];
for i in 0..N_FEATURES {
mask[i] = variances[i] > self.threshold;
}
Ok(FeatureMask::from_array(mask))
}
pub fn transform<const N_SELECTED: usize>(
&self,
X: &FeatureMatrix<f64, N_FEATURES>,
mask: &FeatureMask<N_FEATURES>,
) -> Result<FeatureMatrix<f64, N_SELECTED>> {
X.select_features(mask)
}
pub const fn feature_variances(&self) -> Option<&[f64; N_FEATURES]> {
self.feature_variances.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct UnivariateSelector<const N_FEATURES: usize, const K: usize> {
score_function: UnivariateScoreFunction,
feature_scores: Option<[f64; N_FEATURES]>,
}
impl<const N_FEATURES: usize, const K: usize> UnivariateSelector<N_FEATURES, K> {
pub const fn new(score_function: UnivariateScoreFunction) -> Option<Self> {
if K <= N_FEATURES {
Some(Self {
score_function,
feature_scores: None,
})
} else {
None
}
}
pub fn fit(
&mut self,
X: &FeatureMatrix<f64, N_FEATURES>,
y: ArrayView1<f64>,
) -> Result<FeatureMask<N_FEATURES>> {
let mut scores = [0.0; N_FEATURES];
for i in 0..N_FEATURES {
let feature_index = unsafe { FeatureIndex::new_unchecked(i) };
let feature_data = X.feature(feature_index);
scores[i] = match self.score_function {
UnivariateScoreFunction::Correlation => self.compute_correlation(feature_data, y),
UnivariateScoreFunction::MutualInfo => self.compute_mutual_info(feature_data, y),
UnivariateScoreFunction::Chi2 => self.compute_chi2_score(feature_data, y),
UnivariateScoreFunction::FStatistic => self.compute_f_statistic(feature_data, y),
};
}
self.feature_scores = Some(scores);
let mut indexed_scores: Vec<(usize, f64)> = Vec::with_capacity(N_FEATURES);
for i in 0..N_FEATURES {
indexed_scores.push((i, scores[i]));
}
indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let mut mask = [false; N_FEATURES];
for i in 0..K {
if let Some(&(feature_idx, _)) = indexed_scores.get(i) {
mask[feature_idx] = true;
}
}
Ok(FeatureMask::from_array(mask))
}
pub fn transform(
&self,
X: &FeatureMatrix<f64, N_FEATURES>,
mask: &FeatureMask<N_FEATURES>,
) -> Result<FeatureMatrix<f64, K>> {
X.select_features(mask)
}
pub const fn feature_scores(&self) -> Option<&[f64; N_FEATURES]> {
self.feature_scores.as_ref()
}
fn compute_correlation(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
let n = x.len() as f64;
if n < 2.0 {
return 0.0;
}
let mean_x = x.mean().unwrap_or(0.0);
let mean_y = y.mean().unwrap_or(0.0);
let mut sum_xy = 0.0;
let mut sum_x2 = 0.0;
let mut sum_y2 = 0.0;
for i in 0..x.len() {
let dx = x[i] - mean_x;
let dy = y[i] - mean_y;
sum_xy += dx * dy;
sum_x2 += dx * dx;
sum_y2 += dy * dy;
}
let denom = (sum_x2 * sum_y2).sqrt();
if denom < 1e-10 {
0.0
} else {
(sum_xy / denom).abs()
}
}
fn compute_mutual_info(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
self.compute_correlation(x, y)
}
fn compute_chi2_score(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
self.compute_correlation(x, y)
}
fn compute_f_statistic(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
self.compute_correlation(x, y)
}
}
#[derive(Debug, Clone, Copy)]
pub enum UnivariateScoreFunction {
Correlation,
MutualInfo,
Chi2,
FStatistic,
}
#[derive(Debug, Clone)]
pub struct CorrelationSelector<const N_FEATURES: usize> {
threshold: f64,
correlation_matrix: Option<[[f64; N_FEATURES]; N_FEATURES]>,
}
impl<const N_FEATURES: usize> CorrelationSelector<N_FEATURES> {
pub const fn new(threshold: f64) -> Self {
Self {
threshold,
correlation_matrix: None,
}
}
pub fn fit(&mut self, X: &FeatureMatrix<f64, N_FEATURES>) -> Result<FeatureMask<N_FEATURES>> {
let mut corr_matrix = [[0.0; N_FEATURES]; N_FEATURES];
for i in 0..N_FEATURES {
for j in 0..N_FEATURES {
if i == j {
corr_matrix[i][j] = 1.0;
} else {
let feature_i = unsafe { FeatureIndex::new_unchecked(i) };
let feature_j = unsafe { FeatureIndex::new_unchecked(j) };
let data_i = X.feature(feature_i);
let data_j = X.feature(feature_j);
corr_matrix[i][j] = self.compute_correlation(data_i, data_j);
}
}
}
self.correlation_matrix = Some(corr_matrix);
let mut mask = [true; N_FEATURES];
for i in 0..N_FEATURES {
for j in (i + 1)..N_FEATURES {
if corr_matrix[i][j].abs() > self.threshold && mask[i] && mask[j] {
let feature_i = unsafe { FeatureIndex::new_unchecked(i) };
let feature_j = unsafe { FeatureIndex::new_unchecked(j) };
let var_i = X.feature(feature_i).var(1.0);
let var_j = X.feature(feature_j).var(1.0);
if var_i < var_j {
mask[i] = false;
} else {
mask[j] = false;
}
}
}
}
Ok(FeatureMask::from_array(mask))
}
pub fn transform<const N_SELECTED: usize>(
&self,
X: &FeatureMatrix<f64, N_FEATURES>,
mask: &FeatureMask<N_FEATURES>,
) -> Result<FeatureMatrix<f64, N_SELECTED>> {
X.select_features(mask)
}
pub const fn correlation_matrix(&self) -> Option<&[[f64; N_FEATURES]; N_FEATURES]> {
self.correlation_matrix.as_ref()
}
fn compute_correlation(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
let n = x.len() as f64;
if n < 2.0 {
return 0.0;
}
let mean_x = x.mean().unwrap_or(0.0);
let mean_y = y.mean().unwrap_or(0.0);
let mut sum_xy = 0.0;
let mut sum_x2 = 0.0;
let mut sum_y2 = 0.0;
for i in 0..x.len() {
let dx = x[i] - mean_x;
let dy = y[i] - mean_y;
sum_xy += dx * dy;
sum_x2 += dx * dx;
sum_y2 += dy * dy;
}
let denom = (sum_x2 * sum_y2).sqrt();
if denom < 1e-10 {
0.0
} else {
sum_xy / denom
}
}
}
#[derive(Debug, Clone)]
pub struct TypeSafeSelectionPipeline<const N_FEATURES: usize, State = data_states::Untrained> {
steps: Vec<PipelineStep>,
current_mask: Option<FeatureMask<N_FEATURES>>,
_phantom: PhantomData<State>,
}
impl<const N_FEATURES: usize> Default
for TypeSafeSelectionPipeline<N_FEATURES, data_states::Untrained>
{
fn default() -> Self {
Self::new()
}
}
impl<const N_FEATURES: usize> TypeSafeSelectionPipeline<N_FEATURES, data_states::Untrained> {
pub const fn new() -> Self {
Self {
steps: Vec::new(),
current_mask: None,
_phantom: PhantomData,
}
}
pub fn add_variance_threshold(mut self, threshold: f64) -> Self {
self.steps.push(PipelineStep::VarianceThreshold(threshold));
self
}
pub fn add_correlation_filter(mut self, threshold: f64) -> Self {
self.steps.push(PipelineStep::CorrelationFilter(threshold));
self
}
pub fn add_univariate_selection<const K: usize>(
mut self,
score_function: UnivariateScoreFunction,
) -> Self {
self.steps.push(PipelineStep::UnivariateSelection {
_k: K,
score_function,
});
self
}
pub fn fit(
self,
X: &FeatureMatrix<f64, N_FEATURES>,
y: ArrayView1<f64>,
) -> Result<TypeSafeSelectionPipeline<N_FEATURES, data_states::Trained>> {
let mut current_mask = FeatureMask::all_selected();
for step in &self.steps {
let step_mask = match step {
PipelineStep::VarianceThreshold(threshold) => {
let mut selector = VarianceThresholdSelector::new(*threshold);
selector.fit(X)?
}
PipelineStep::CorrelationFilter(threshold) => {
let mut selector = CorrelationSelector::new(*threshold);
selector.fit(X)?
}
PipelineStep::UnivariateSelection {
_k: _,
score_function,
} => {
const DEFAULT_K: usize = 10;
if DEFAULT_K <= N_FEATURES {
let mut selector =
UnivariateSelector::<N_FEATURES, DEFAULT_K>::new(*score_function)
.ok_or_else(|| {
SklearsError::InvalidInput(
"Invalid K for univariate selection".to_string(),
)
})?;
selector.fit(X, y)?
} else {
FeatureMask::all_selected()
}
}
};
current_mask = current_mask.and(&step_mask);
}
Ok(TypeSafeSelectionPipeline {
steps: self.steps,
current_mask: Some(current_mask),
_phantom: PhantomData,
})
}
}
impl<const N_FEATURES: usize> TypeSafeSelectionPipeline<N_FEATURES, data_states::Trained> {
pub fn transform<const N_SELECTED: usize>(
&self,
X: &FeatureMatrix<f64, N_FEATURES>,
) -> Result<FeatureMatrix<f64, N_SELECTED>> {
if let Some(ref mask) = self.current_mask {
X.select_features(mask)
} else {
Err(SklearsError::FitError("Pipeline not fitted".to_string()))
}
}
pub fn selection_mask(&self) -> Option<&FeatureMask<N_FEATURES>> {
self.current_mask.as_ref()
}
pub fn n_selected_features(&self) -> usize {
self.current_mask
.as_ref()
.map(|mask| mask.count_selected())
.unwrap_or(0)
}
}
#[derive(Debug, Clone)]
enum PipelineStep {
VarianceThreshold(f64),
CorrelationFilter(f64),
UnivariateSelection {
_k: usize,
score_function: UnivariateScoreFunction,
},
}
pub trait ZeroCostTransform<Input, Output> {
fn transform_zero_cost(input: Input) -> Output;
}
impl<const N: usize> ZeroCostTransform<FeatureIndex<N>, usize> for () {
fn transform_zero_cost(input: FeatureIndex<N>) -> usize {
input.get()
}
}
impl<const N: usize> ZeroCostTransform<FeatureMask<N>, Vec<bool>> for () {
fn transform_zero_cost(input: FeatureMask<N>) -> Vec<bool> {
input.as_array().to_vec()
}
}
pub struct FeatureCountValidator<const EXPECTED: usize>;
impl<const EXPECTED: usize> FeatureCountValidator<EXPECTED> {
pub const fn validate<const ACTUAL: usize>() -> bool {
EXPECTED == ACTUAL
}
pub fn validate_matrix<T>(matrix: FeatureMatrix<T, EXPECTED>) -> FeatureMatrix<T, EXPECTED>
where
T: Clone + Default,
{
matrix
}
}
pub trait TypeSafeFeatureSelection {
type FeatureMatrix;
type SelectionResult;
const INPUT_FEATURES: usize;
fn select_features_typed(data: Self::FeatureMatrix) -> Result<Self::SelectionResult>;
}
#[macro_export]
macro_rules! impl_type_safe_selector {
($selector:ty, $method:ty, $n_features:expr, $n_selected:expr) => {
impl TypeSafeFeatureSelection for $selector {
type FeatureMatrix = FeatureMatrix<f64, $n_features>;
type SelectionResult = FeatureMatrix<f64, $n_selected>;
const INPUT_FEATURES: usize = $n_features;
fn select_features_typed(data: Self::FeatureMatrix) -> Result<Self::SelectionResult> {
use $crate::type_safe::VarianceThresholdSelector;
let mut selector = VarianceThresholdSelector::<$n_features>::new(0.0);
let mask = selector.fit(&data)?;
if mask.count_selected() != $n_selected {
return Err(SklearsError::InvalidInput(format!(
"Expected {} selected features, got {}. Consider adjusting selection parameters.",
$n_selected,
mask.count_selected()
)));
}
data.select_features(&mask)
}
}
};
}
pub const fn binomial_coefficient(n: usize, k: usize) -> usize {
if k > n {
0
} else if k == 0 || k == n {
1
} else {
let k = if k > n - k { n - k } else { k };
let mut result = 1;
let mut i = 0;
while i < k {
result = result * (n - i) / (i + 1);
i += 1;
}
result
}
}
pub const fn validate_selection_count<const N_FEATURES: usize, const K: usize>() -> bool {
K <= N_FEATURES && K > 0
}
pub trait TypeBool {
const VALUE: bool;
}
pub struct True;
pub struct False;
impl TypeBool for True {
const VALUE: bool = true;
}
impl TypeBool for False {
const VALUE: bool = false;
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_feature_index() {
const MAX_FEATURES: usize = 10;
let valid_index = FeatureIndex::<MAX_FEATURES>::new(5).expect("operation should succeed");
assert_eq!(valid_index.get(), 5);
assert!(FeatureIndex::<MAX_FEATURES>::new(15).is_none());
}
#[test]
fn test_feature_mask() {
const N_FEATURES: usize = 5;
let mask = FeatureMask::<N_FEATURES>::from_array([true, false, true, false, true]);
assert_eq!(mask.count_selected(), 3);
let indices = mask.selected_indices();
assert_eq!(indices.len(), 3);
}
#[test]
fn test_feature_matrix() -> Result<()> {
const N_FEATURES: usize = 3;
let data = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let matrix = FeatureMatrix::<f64, N_FEATURES>::new(data)?;
assert_eq!(matrix.n_features(), 3);
assert_eq!(matrix.n_samples(), 2);
Ok(())
}
#[test]
fn test_variance_threshold_selector() -> Result<()> {
const N_FEATURES: usize = 3;
let data = array![[1.0, 2.0, 3.0], [1.1, 5.0, 3.1], [0.9, 8.0, 2.9]];
let matrix = FeatureMatrix::<f64, N_FEATURES>::new(data)?;
let mut selector = VarianceThresholdSelector::new(0.1);
let mask = selector.fit(&matrix)?;
assert!(mask.count_selected() > 0);
Ok(())
}
#[test]
fn test_compile_time_validation() {
const N_FEATURES: usize = 10;
const K: usize = 5;
assert!(validate_selection_count::<N_FEATURES, K>());
}
#[test]
fn test_type_safe_pipeline() -> Result<()> {
const N_FEATURES: usize = 4;
let data = array![
[1.0, 2.0, 3.0, 4.0],
[1.1, 5.0, 3.1, 4.1],
[0.9, 8.0, 2.9, 3.9],
[1.2, 2.1, 3.2, 4.2]
];
let matrix = FeatureMatrix::<f64, N_FEATURES>::new(data)?;
let y = array![0.0, 1.0, 0.0, 1.0];
let pipeline = TypeSafeSelectionPipeline::<N_FEATURES>::new()
.add_variance_threshold(0.01)
.add_correlation_filter(0.9);
let fitted_pipeline = pipeline.fit(&matrix, y.view())?;
assert!(fitted_pipeline.n_selected_features() > 0);
Ok(())
}
}