use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use sklears_core::error::{Result as SklResult, SklearsError};
type Result<T> = SklResult<T>;
use crate::base::{FeatureSelector, SelectorMixin};
use sklears_core::traits::{Estimator, Fit, Transform};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FilterError {
#[error("Invalid number of features to select: {0}")]
InvalidFeatureCount(usize),
#[error("Invalid percentile: {0}, must be between 0 and 100")]
InvalidPercentile(f64),
#[error("Insufficient variance for threshold: {0}")]
InsufficientVariance(f64),
#[error("Empty feature matrix")]
EmptyFeatureMatrix,
#[error("Feature selection failed: {0}")]
SelectionFailed(String),
}
impl From<FilterError> for SklearsError {
fn from(err: FilterError) -> Self {
SklearsError::FitError(format!("Filter selection error: {}", err))
}
}
pub type ScoreFunc = fn(ArrayView2<f64>, ArrayView1<f64>) -> Result<Array1<f64>>;
#[derive(Debug, Clone)]
pub struct FilterConfig {
pub score_func: String,
pub k: Option<usize>,
pub percentile: Option<f64>,
pub threshold: Option<f64>,
}
impl Default for FilterConfig {
fn default() -> Self {
Self {
score_func: "f_classif".to_string(),
k: Some(10),
percentile: None,
threshold: None,
}
}
}
#[derive(Debug, Clone)]
pub struct FilterResults {
pub scores: Array1<f64>,
pub selected_features: Vec<usize>,
pub feature_names: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct SelectKBest {
pub k: usize,
pub score_func: String,
}
impl SelectKBest {
pub fn new(k: usize, score_func: &str) -> Self {
Self {
k,
score_func: score_func.to_string(),
}
}
}
impl Estimator for SelectKBest {
type Config = FilterConfig;
type Error = FilterError;
type Float = f64;
fn config(&self) -> &Self::Config {
static CONFIG: std::sync::OnceLock<FilterConfig> = std::sync::OnceLock::new();
CONFIG.get_or_init(FilterConfig::default)
}
fn check_compatibility(&self, _n_samples: usize, n_features: usize) -> Result<()> {
if self.k > n_features {
return Err(FilterError::InvalidFeatureCount(self.k).into());
}
Ok(())
}
}
impl<'a> Fit<ArrayView2<'a, f64>, ArrayView1<'a, f64>> for SelectKBest {
type Fitted = SelectKBestTrained;
fn fit(self, X: &ArrayView2<'a, f64>, y: &ArrayView1<'a, f64>) -> Result<Self::Fitted> {
self.fit_impl(X, y)
}
}
impl Fit<Array2<f64>, Array1<i32>> for SelectKBest {
type Fitted = SelectKBestTrained;
fn fit(self, X: &Array2<f64>, y: &Array1<i32>) -> Result<Self::Fitted> {
let y_f64: Array1<f64> = y.mapv(|x| x as f64);
self.fit_impl(&X.view(), &y_f64.view())
}
}
impl SelectKBest {
fn fit_impl(self, X: &ArrayView2<f64>, y: &ArrayView1<f64>) -> Result<SelectKBestTrained> {
if X.is_empty() || y.is_empty() {
return Err(FilterError::EmptyFeatureMatrix.into());
}
if self.k == 0 || self.k > X.ncols() {
return Err(FilterError::InvalidFeatureCount(self.k).into());
}
let mut scores = Array1::zeros(X.ncols());
for i in 0..X.ncols() {
let feature = X.column(i);
scores[i] = self.compute_correlation(feature, *y);
}
let mut indexed_scores: Vec<(usize, f64)> = scores
.iter()
.enumerate()
.map(|(i, &score)| (i, score.abs()))
.collect();
indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
let selected_features: Vec<usize> = indexed_scores
.into_iter()
.take(self.k)
.map(|(idx, _)| idx)
.collect();
Ok(SelectKBestTrained {
selected_features,
scores,
k: self.k,
})
}
}
impl SelectKBest {
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 SelectKBestTrained {
pub selected_features: Vec<usize>,
pub scores: Array1<f64>,
pub k: usize,
}
impl Transform<ArrayView2<'_, f64>, Array2<f64>> for SelectKBestTrained {
fn transform(&self, X: &ArrayView2<'_, f64>) -> Result<Array2<f64>> {
self.transform_impl(&X.view())
}
}
impl Transform<Array2<f64>, Array2<f64>> for SelectKBestTrained {
fn transform(&self, X: &Array2<f64>) -> Result<Array2<f64>> {
self.transform_impl(&X.view())
}
}
impl SelectorMixin for SelectKBestTrained {
fn get_support(&self) -> Result<Array1<bool>> {
let n_features = self.scores.len();
let mut support = Array1::from_elem(n_features, false);
for &idx in &self.selected_features {
if idx < support.len() {
support[idx] = true;
}
}
Ok(support)
}
fn transform_features(&self, indices: &[usize]) -> Result<Vec<usize>> {
Ok(indices
.iter()
.filter_map(|&idx| self.selected_features.iter().position(|&f| f == idx))
.collect())
}
}
impl FeatureSelector for SelectKBestTrained {
fn selected_features(&self) -> &Vec<usize> {
&self.selected_features
}
}
impl SelectKBestTrained {
fn transform_impl(&self, X: &ArrayView2<f64>) -> Result<Array2<f64>> {
if self.selected_features.is_empty() {
return Err(FilterError::SelectionFailed("No features selected".to_string()).into());
}
let n_samples = X.nrows();
let mut transformed = Array2::zeros((n_samples, self.selected_features.len()));
for (new_idx, &orig_idx) in self.selected_features.iter().enumerate() {
if orig_idx < X.ncols() {
transformed.column_mut(new_idx).assign(&X.column(orig_idx));
}
}
Ok(transformed)
}
}
#[derive(Debug, Clone)]
pub struct SelectPercentile {
pub percentile: f64,
pub score_func: String,
}
impl SelectPercentile {
pub fn new(percentile: f64, score_func: &str) -> Self {
Self {
percentile,
score_func: score_func.to_string(),
}
}
}
impl Estimator for SelectPercentile {
type Config = FilterConfig;
type Error = FilterError;
type Float = f64;
fn config(&self) -> &Self::Config {
static CONFIG: std::sync::OnceLock<FilterConfig> = std::sync::OnceLock::new();
CONFIG.get_or_init(FilterConfig::default)
}
fn check_compatibility(&self, _n_samples: usize, _n_features: usize) -> Result<()> {
if self.percentile <= 0.0 || self.percentile > 100.0 {
return Err(FilterError::InvalidPercentile(self.percentile).into());
}
Ok(())
}
}
impl<'a> Fit<ArrayView2<'a, f64>, ArrayView1<'a, f64>> for SelectPercentile {
type Fitted = SelectPercentileTrained;
fn fit(self, X: &ArrayView2<'a, f64>, y: &ArrayView1<'a, f64>) -> Result<Self::Fitted> {
if X.is_empty() || y.is_empty() {
return Err(FilterError::EmptyFeatureMatrix.into());
}
if self.percentile <= 0.0 || self.percentile > 100.0 {
return Err(FilterError::InvalidPercentile(self.percentile).into());
}
let mut scores = Array1::zeros(X.ncols());
for i in 0..X.ncols() {
let feature = X.column(i);
scores[i] = self.compute_correlation(feature, *y).abs();
}
let mut sorted_scores: Vec<f64> = scores.to_vec();
sorted_scores.sort_by(|a, b| b.partial_cmp(a).expect("operation should succeed"));
let threshold_idx =
((100.0 - self.percentile) / 100.0 * sorted_scores.len() as f64) as usize;
let threshold = sorted_scores.get(threshold_idx).copied().unwrap_or(0.0);
let selected_features: Vec<usize> = scores
.iter()
.enumerate()
.filter(|(_, &score)| score >= threshold)
.map(|(idx, _)| idx)
.collect();
Ok(SelectPercentileTrained {
selected_features,
scores,
percentile: self.percentile,
threshold,
})
}
}
impl SelectPercentile {
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 SelectPercentileTrained {
pub selected_features: Vec<usize>,
pub scores: Array1<f64>,
pub percentile: f64,
pub threshold: f64,
}
impl Transform<ArrayView2<'_, f64>, Array2<f64>> for SelectPercentileTrained {
fn transform(&self, X: &ArrayView2<'_, f64>) -> Result<Array2<f64>> {
if self.selected_features.is_empty() {
return Err(FilterError::SelectionFailed("No features selected".to_string()).into());
}
let n_samples = X.nrows();
let mut transformed = Array2::zeros((n_samples, self.selected_features.len()));
for (new_idx, &orig_idx) in self.selected_features.iter().enumerate() {
if orig_idx < X.ncols() {
transformed.column_mut(new_idx).assign(&X.column(orig_idx));
}
}
Ok(transformed)
}
}
#[derive(Debug, Clone)]
pub struct VarianceThreshold {
pub threshold: f64,
}
impl VarianceThreshold {
pub fn new(threshold: f64) -> Self {
Self { threshold }
}
}
impl Default for VarianceThreshold {
fn default() -> Self {
Self { threshold: 0.0 }
}
}
impl Estimator for VarianceThreshold {
type Config = FilterConfig;
type Error = FilterError;
type Float = f64;
fn config(&self) -> &Self::Config {
static CONFIG: std::sync::OnceLock<FilterConfig> = std::sync::OnceLock::new();
CONFIG.get_or_init(FilterConfig::default)
}
fn check_compatibility(&self, _n_samples: usize, _n_features: usize) -> Result<()> {
if self.threshold < 0.0 {
return Err(FilterError::InsufficientVariance(self.threshold).into());
}
Ok(())
}
}
impl<'a> Fit<ArrayView2<'a, f64>, ArrayView1<'a, f64>> for VarianceThreshold {
type Fitted = VarianceThresholdTrained;
fn fit(self, X: &ArrayView2<'a, f64>, _y: &ArrayView1<'a, f64>) -> Result<Self::Fitted> {
self.fit_impl(X)
}
}
impl Fit<Array2<f64>, Array1<i32>> for VarianceThreshold {
type Fitted = VarianceThresholdTrained;
fn fit(self, X: &Array2<f64>, _y: &Array1<i32>) -> Result<Self::Fitted> {
self.fit_impl(&X.view())
}
}
impl VarianceThreshold {
fn fit_impl(self, X: &ArrayView2<f64>) -> Result<VarianceThresholdTrained> {
if X.is_empty() {
return Err(FilterError::EmptyFeatureMatrix.into());
}
let mut variances = Array1::zeros(X.ncols());
let mut selected_features = Vec::new();
for i in 0..X.ncols() {
let feature = X.column(i);
let variance = feature.var(1.0);
variances[i] = variance;
if variance > self.threshold {
selected_features.push(i);
}
}
Ok(VarianceThresholdTrained {
selected_features,
variances,
threshold: self.threshold,
})
}
}
#[derive(Debug, Clone)]
pub struct VarianceThresholdTrained {
pub selected_features: Vec<usize>,
pub variances: Array1<f64>,
pub threshold: f64,
}
impl Transform<ArrayView2<'_, f64>, Array2<f64>> for VarianceThresholdTrained {
fn transform(&self, X: &ArrayView2<'_, f64>) -> Result<Array2<f64>> {
self.transform_impl(&X.view())
}
}
impl Transform<Array2<f64>, Array2<f64>> for VarianceThresholdTrained {
fn transform(&self, X: &Array2<f64>) -> Result<Array2<f64>> {
self.transform_impl(&X.view())
}
}
impl SelectorMixin for VarianceThresholdTrained {
fn get_support(&self) -> Result<Array1<bool>> {
let n_features = self.variances.len();
let mut support = Array1::from_elem(n_features, false);
for &idx in &self.selected_features {
if idx < support.len() {
support[idx] = true;
}
}
Ok(support)
}
fn transform_features(&self, indices: &[usize]) -> Result<Vec<usize>> {
Ok(indices
.iter()
.filter_map(|&idx| self.selected_features.iter().position(|&f| f == idx))
.collect())
}
}
impl FeatureSelector for VarianceThresholdTrained {
fn selected_features(&self) -> &Vec<usize> {
&self.selected_features
}
}
impl VarianceThresholdTrained {
fn transform_impl(&self, X: &ArrayView2<f64>) -> Result<Array2<f64>> {
if self.selected_features.is_empty() {
return Err(FilterError::SelectionFailed(
"All features removed by variance threshold".to_string(),
)
.into());
}
let n_samples = X.nrows();
let mut transformed = Array2::zeros((n_samples, self.selected_features.len()));
for (new_idx, &orig_idx) in self.selected_features.iter().enumerate() {
if orig_idx < X.ncols() {
transformed.column_mut(new_idx).assign(&X.column(orig_idx));
}
}
Ok(transformed)
}
}
#[derive(Debug, Clone)]
pub struct GenericUnivariateSelect {
pub score_func: String,
pub mode: String,
pub param: f64,
}
impl GenericUnivariateSelect {
pub fn new(score_func: &str, mode: &str, param: f64) -> Self {
Self {
score_func: score_func.to_string(),
mode: mode.to_string(),
param,
}
}
}
impl Estimator for GenericUnivariateSelect {
type Config = FilterConfig;
type Error = FilterError;
type Float = f64;
fn config(&self) -> &Self::Config {
static CONFIG: std::sync::OnceLock<FilterConfig> = std::sync::OnceLock::new();
CONFIG.get_or_init(FilterConfig::default)
}
fn check_compatibility(&self, _n_samples: usize, n_features: usize) -> Result<()> {
if self.mode == "k_best" && (self.param as usize) > n_features {
return Err(FilterError::InvalidFeatureCount(self.param as usize).into());
}
Ok(())
}
}
impl<'a> Fit<ArrayView2<'a, f64>, ArrayView1<'a, f64>> for GenericUnivariateSelect {
type Fitted = GenericUnivariateSelectTrained;
fn fit(self, X: &ArrayView2<'a, f64>, y: &ArrayView1<'a, f64>) -> Result<Self::Fitted> {
let k_best = SelectKBest::new(self.param as usize, &self.score_func);
let trained = k_best.fit(X, y)?;
Ok(GenericUnivariateSelectTrained {
selected_features: trained.selected_features,
scores: trained.scores,
})
}
}
#[derive(Debug, Clone)]
pub struct GenericUnivariateSelectTrained {
pub selected_features: Vec<usize>,
pub scores: Array1<f64>,
}
impl Transform<ArrayView2<'_, f64>, Array2<f64>> for GenericUnivariateSelectTrained {
fn transform(&self, X: &ArrayView2<'_, f64>) -> Result<Array2<f64>> {
let n_samples = X.nrows();
let mut transformed = Array2::zeros((n_samples, self.selected_features.len()));
for (new_idx, &orig_idx) in self.selected_features.iter().enumerate() {
if orig_idx < X.ncols() {
transformed.column_mut(new_idx).assign(&X.column(orig_idx));
}
}
Ok(transformed)
}
}
#[derive(Debug, Clone)]
pub struct CorrelationThreshold {
pub threshold: f64,
}
impl CorrelationThreshold {
pub fn new(threshold: f64) -> Self {
Self { threshold }
}
}
impl Estimator for CorrelationThreshold {
type Config = FilterConfig;
type Error = FilterError;
type Float = f64;
fn config(&self) -> &Self::Config {
static CONFIG: std::sync::OnceLock<FilterConfig> = std::sync::OnceLock::new();
CONFIG.get_or_init(FilterConfig::default)
}
fn check_compatibility(&self, _n_samples: usize, _n_features: usize) -> Result<()> {
if self.threshold < 0.0 || self.threshold > 1.0 {
return Err(FilterError::InvalidPercentile(self.threshold).into());
}
Ok(())
}
}
impl<'a> Fit<ArrayView2<'a, f64>, ArrayView1<'a, f64>> for CorrelationThreshold {
type Fitted = CorrelationThresholdTrained;
fn fit(self, _X: &ArrayView2<'a, f64>, _y: &ArrayView1<'a, f64>) -> Result<Self::Fitted> {
Err(SklearsError::NotImplemented(
"CorrelationThreshold selector not yet implemented".to_string(),
))
}
}
#[derive(Debug, Clone)]
pub struct CorrelationThresholdTrained {
pub selected_features: Vec<usize>,
}
impl Transform<ArrayView2<'_, f64>, Array2<f64>> for CorrelationThresholdTrained {
fn transform(&self, X: &ArrayView2<'_, f64>) -> Result<Array2<f64>> {
let n_samples = X.nrows();
let mut transformed = Array2::zeros((n_samples, self.selected_features.len()));
for (new_idx, &orig_idx) in self.selected_features.iter().enumerate() {
if orig_idx < X.ncols() {
transformed.column_mut(new_idx).assign(&X.column(orig_idx));
}
}
Ok(transformed)
}
}
macro_rules! impl_stub_selector {
($name:ident, $trained:ident) => {
#[derive(Debug, Clone)]
pub struct $name;
impl Estimator for $name {
type Config = FilterConfig;
type Error = FilterError;
type Float = f64;
fn config(&self) -> &Self::Config {
static CONFIG: std::sync::OnceLock<FilterConfig> = std::sync::OnceLock::new();
CONFIG.get_or_init(|| FilterConfig::default())
}
}
impl<'a> Fit<ArrayView2<'a, f64>, ArrayView1<'a, f64>> for $name {
type Fitted = $trained;
fn fit(
self,
_X: &ArrayView2<'a, f64>,
_y: &ArrayView1<'a, f64>,
) -> Result<Self::Fitted> {
Err(SklearsError::NotImplemented(
concat!(stringify!($name), " selector not yet implemented").to_string(),
)
.into())
}
}
#[derive(Debug, Clone)]
pub struct $trained {
/// selected_features
pub selected_features: Vec<usize>,
}
impl Transform<ArrayView2<'_, f64>, Array2<f64>> for $trained {
fn transform(&self, X: &ArrayView2<'_, f64>) -> Result<Array2<f64>> {
let n_samples = X.nrows();
let mut transformed = Array2::zeros((n_samples, self.selected_features.len()));
for (new_idx, &orig_idx) in self.selected_features.iter().enumerate() {
if orig_idx < X.ncols() {
transformed.column_mut(new_idx).assign(&X.column(orig_idx));
}
}
Ok(transformed)
}
}
};
}
impl_stub_selector!(SelectFpr, SelectFprTrained);
impl_stub_selector!(SelectFdr, SelectFdrTrained);
impl_stub_selector!(SelectFwe, SelectFweTrained);
impl_stub_selector!(Relief, ReliefTrained);
impl_stub_selector!(ReliefF, ReliefFTrained);
impl_stub_selector!(RReliefF, RReliefFTrained);
impl_stub_selector!(SureIndependenceScreening, SureIndependenceScreeningTrained);
impl_stub_selector!(KnockoffSelector, KnockoffSelectorTrained);
impl_stub_selector!(HighDimensionalInference, HighDimensionalInferenceTrained);
impl_stub_selector!(CompressedSensingSelector, CompressedSensingSelectorTrained);
impl_stub_selector!(ImbalancedDataSelector, ImbalancedDataSelectorTrained);
impl_stub_selector!(SelectKBestParallel, SelectKBestParallelTrained);
#[derive(Debug, Clone)]
pub enum CompressedSensingAlgorithm {
OMP,
CoSaMP,
IHT,
SP,
}
#[derive(Debug, Clone)]
pub enum InferenceMethod {
Lasso,
Ridge,
ElasticNet,
PostSelection,
}
#[derive(Debug, Clone)]
pub enum KnockoffType {
Equicorrelated,
SDP,
FixedDesign,
}
#[derive(Debug, Clone)]
pub enum ImbalancedStrategy {
MinorityFocused,
CostSensitive,
EnsembleImbalanced,
SMOTEEnhanced,
WeightedSelection,
}