use ferrolearn_core::traits::Fit;
use ferrolearn_preprocess::feature_selection::{ScoreFunc, SelectKBest, VarianceThreshold};
use ndarray::{Array1, Array2, array};
const TIE_SCORES_INFORMATIVE: [f64; 2] = [72.0, 72.0]; const TIE_K1_SUPPORT: [usize; 1] = [1];
const TIE_K2_SUPPORT: [usize; 2] = [0, 1];
const K_OVER_NFEAT_SUPPORT: [usize; 3] = [0, 1, 2];
const DISTINCT_SCORES: [f64; 3] = [98.0, 24.2, 0.125];
const DISTINCT_K2_SUPPORT: [usize; 2] = [0, 1];
const VT_VARIANCES: [f64; 3] = [0.666_666_666_666_666_6, 0.0, 0.0];
#[test]
fn divergence_selectkbest_tiebreak_keeps_higher_index() {
let x: Array2<f64> = array![[1., 1., 5.], [2., 2., 5.], [7., 7., 5.], [8., 8., 5.]];
let y: Array1<usize> = array![0, 0, 1, 1];
let sel = SelectKBest::<f64>::new(1, ScoreFunc::FClassif);
let fitted = match sel.fit(&x, &y) {
Ok(f) => f,
Err(e) => panic!("fit failed: {e:?}"),
};
assert_eq!(
fitted.selected_indices(),
&TIE_K1_SUPPORT[..],
"sklearn argsort-mergesort[-1:] keeps the HIGHER index of the tie"
);
}
#[test]
fn divergence_selectkbest_constant_feature_ranks_last() {
let x: Array2<f64> = array![[1., 1., 5.], [2., 2., 5.], [7., 7., 5.], [8., 8., 5.]];
let y: Array1<usize> = array![0, 0, 1, 1];
let sel = SelectKBest::<f64>::new(2, ScoreFunc::FClassif);
let fitted = match sel.fit(&x, &y) {
Ok(f) => f,
Err(e) => panic!("fit failed: {e:?}"),
};
assert_eq!(
fitted.selected_indices(),
&TIE_K2_SUPPORT[..],
"constant col 2 (NaN->finfo.min in sklearn) must NOT be selected at k=2"
);
}
#[test]
fn divergence_selectkbest_k_over_nfeatures_keeps_all() {
let x: Array2<f64> = array![[1., 1., 5.], [2., 2., 5.], [7., 7., 5.], [8., 8., 5.]];
let y: Array1<usize> = array![0, 0, 1, 1];
let sel = SelectKBest::<f64>::new(10, ScoreFunc::FClassif);
let fitted = match sel.fit(&x, &y) {
Ok(f) => f,
Err(e) => panic!("sklearn keeps all features when k>n_features; ferrolearn errored: {e:?}"),
};
assert_eq!(
fitted.selected_indices(),
&K_OVER_NFEAT_SUPPORT[..],
"sklearn warns and keeps ALL features when k>n_features"
);
}
#[test]
fn guard_selectkbest_scores_match_sklearn_finite() {
let x: Array2<f64> = array![[1., 1., 5.], [2., 2., 5.], [7., 7., 5.], [8., 8., 5.]];
let y: Array1<usize> = array![0, 0, 1, 1];
let sel = SelectKBest::<f64>::new(2, ScoreFunc::FClassif);
let fitted = match sel.fit(&x, &y) {
Ok(f) => f,
Err(e) => panic!("fit failed: {e:?}"),
};
let scores = fitted.scores();
assert!(
(scores[0] - TIE_SCORES_INFORMATIVE[0]).abs() < 1e-9,
"col0 F={} expected {}",
scores[0],
TIE_SCORES_INFORMATIVE[0]
);
assert!(
(scores[1] - TIE_SCORES_INFORMATIVE[1]).abs() < 1e-9,
"col1 F={} expected {}",
scores[1],
TIE_SCORES_INFORMATIVE[1]
);
}
#[test]
fn guard_selectkbest_distinct_topk_matches() {
let x: Array2<f64> = array![[1., 1., 1.], [2., 3., 5.], [8., 7., 2.], [9., 8., 6.]];
let y: Array1<usize> = array![0, 0, 1, 1];
let sel = SelectKBest::<f64>::new(2, ScoreFunc::FClassif);
let fitted = match sel.fit(&x, &y) {
Ok(f) => f,
Err(e) => panic!("fit failed: {e:?}"),
};
let scores = fitted.scores();
for j in 0..3 {
assert!(
(scores[j] - DISTINCT_SCORES[j]).abs() < 1e-9,
"col{j} F={} expected {}",
scores[j],
DISTINCT_SCORES[j]
);
}
assert_eq!(fitted.selected_indices(), &DISTINCT_K2_SUPPORT[..]);
}
#[test]
fn guard_selectkbest_perfect_separator_is_inf_and_kept() {
let x: Array2<f64> = array![[1.], [1.], [9.], [9.]];
let y: Array1<usize> = array![0, 0, 1, 1];
let sel = SelectKBest::<f64>::new(1, ScoreFunc::FClassif);
let fitted = match sel.fit(&x, &y) {
Ok(f) => f,
Err(e) => panic!("fit failed: {e:?}"),
};
assert!(
fitted.scores()[0].is_infinite(),
"perfect separator F must be +inf (matches sklearn inf)"
);
assert_eq!(fitted.selected_indices(), &[0usize][..]);
}
#[test]
fn guard_variance_threshold_zero_drops_constants() {
let x: Array2<f64> = array![[1., 5., 2.], [2., 5., 2.], [3., 5., 2.]];
let sel = VarianceThreshold::<f64>::new(0.0);
let fitted = match sel.fit(&x, &()) {
Ok(f) => f,
Err(e) => panic!("fit failed: {e:?}"),
};
let v = fitted.variances();
for j in 0..3 {
assert!(
(v[j] - VT_VARIANCES[j]).abs() < 1e-9,
"var col{j}={} expected {}",
v[j],
VT_VARIANCES[j]
);
}
assert_eq!(fitted.selected_indices(), &[0usize][..]);
}
#[test]
fn guard_variance_threshold_custom_threshold() {
let x: Array2<f64> = array![[1., 10.], [2., 20.], [3., 30.]];
let sel = VarianceThreshold::<f64>::new(0.5);
let fitted = match sel.fit(&x, &()) {
Ok(f) => f,
Err(e) => panic!("fit failed: {e:?}"),
};
assert_eq!(fitted.selected_indices(), &[0usize, 1][..]);
let v = fitted.variances();
assert!((v[0] - 0.666_666_666_666_666_6).abs() < 1e-9);
assert!((v[1] - 66.666_666_666_666_67).abs() < 1e-9);
}
#[test]
fn guard_variance_threshold_negative_threshold_errors() {
let x: Array2<f64> = array![[1.], [2.]];
let sel = VarianceThreshold::<f64>::new(-0.1);
assert!(sel.fit(&x, &()).is_err());
}
#[test]
fn guard_variance_threshold_zero_rows_errors() {
let x: Array2<f64> = Array2::zeros((0, 2));
let sel = VarianceThreshold::<f64>::new(0.0);
assert!(sel.fit(&x, &()).is_err());
}
#[test]
fn guard_selectkbest_zero_rows_errors() {
let x: Array2<f64> = Array2::zeros((0, 3));
let y: Array1<usize> = Array1::zeros(0);
let sel = SelectKBest::<f64>::new(1, ScoreFunc::FClassif);
assert!(sel.fit(&x, &y).is_err());
}
#[test]
fn guard_selectkbest_y_length_mismatch_errors() {
let x: Array2<f64> = array![[1., 2.], [3., 4.]];
let y: Array1<usize> = array![0]; let sel = SelectKBest::<f64>::new(1, ScoreFunc::FClassif);
assert!(sel.fit(&x, &y).is_err());
}
#[test]
fn guard_variance_threshold_f32() {
let x: Array2<f32> = array![[1.0f32, 5.0], [2.0, 5.0], [3.0, 5.0]];
let sel = VarianceThreshold::<f32>::new(0.0f32);
let fitted = match sel.fit(&x, &()) {
Ok(f) => f,
Err(e) => panic!("fit failed: {e:?}"),
};
assert_eq!(fitted.selected_indices(), &[0usize][..]);
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false,..) is the test-failure signal in the fit-Err arm"
)]
fn reaudit_a_four_way_tie_k2_keeps_two_highest() {
let x: Array2<f64> = array![
[1., 1., 1., 1.],
[2., 2., 2., 2.],
[7., 7., 7., 7.],
[8., 8., 8., 8.]
];
let y: Array1<usize> = array![0, 0, 1, 1];
let sel = SelectKBest::<f64>::new(2, ScoreFunc::FClassif);
let fitted = match sel.fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "fit failed: {e:?}");
return;
}
};
assert_eq!(
fitted.selected_indices(),
&[2usize, 3][..],
"4-way tie k=2 must keep the two HIGHEST indices (sklearn argsort-mergesort[-2:])"
);
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false,..) is the test-failure signal in the fit-Err arm"
)]
fn reaudit_b_two_constants_plus_informative_k3_k4() {
let x: Array2<f64> = array![
[9., 9., 1., 1., 1.],
[9., 9., 2., 3., 5.],
[9., 9., 8., 7., 2.],
[9., 9., 9., 8., 6.]
];
let y: Array1<usize> = array![0, 0, 1, 1];
let sel3 = SelectKBest::<f64>::new(3, ScoreFunc::FClassif);
let f3 = match sel3.fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "k=3 fit failed: {e:?}");
return;
}
};
assert_eq!(
f3.selected_indices(),
&[2usize, 3, 4][..],
"k=3 must pick the 3 informative cols; both constants (NaN->finfo.min) rank last"
);
let sel4 = SelectKBest::<f64>::new(4, ScoreFunc::FClassif);
let f4 = match sel4.fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "k=4 fit failed: {e:?}");
return;
}
};
assert_eq!(
f4.selected_indices(),
&[1usize, 2, 3, 4][..],
"k=4 forces one constant; sklearn keeps the HIGHER-index constant (col 1, not col 0)"
);
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false,..) is the test-failure signal in the fit-Err arm"
)]
fn reaudit_c_k_boundaries_all_none_argmax() {
let x: Array2<f64> = array![[1., 1., 1.], [2., 3., 5.], [8., 7., 2.], [9., 8., 6.]];
let y: Array1<usize> = array![0, 0, 1, 1];
let f_all = match SelectKBest::<f64>::new(3, ScoreFunc::FClassif).fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "k=all fit failed: {e:?}");
return;
}
};
assert_eq!(
f_all.selected_indices(),
&[0usize, 1, 2][..],
"k==n_features selects all"
);
let f_none = match SelectKBest::<f64>::new(0, ScoreFunc::FClassif).fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "k=0 fit failed: {e:?}");
return;
}
};
assert!(
f_none.selected_indices().is_empty(),
"k==0 selects NONE (sklearn _get_support_mask k==0 -> all-false), got {:?}",
f_none.selected_indices()
);
let f_one = match SelectKBest::<f64>::new(1, ScoreFunc::FClassif).fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "k=1 fit failed: {e:?}");
return;
}
};
assert_eq!(
f_one.selected_indices(),
&[0usize][..],
"k==1 picks the single argmax (col 0, F=98)"
);
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false,..) is the test-failure signal in the fit-Err arm"
)]
fn reaudit_d_k_over_nfeatures_clamps_keep_all() {
let x: Array2<f64> = array![
[1., 1., 1., 1.],
[2., 3., 5., 2.],
[8., 7., 2., 9.],
[9., 8., 6., 1.]
];
let y: Array1<usize> = array![0, 0, 1, 1];
let sel = SelectKBest::<f64>::new(7, ScoreFunc::FClassif);
let fitted = match sel.fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "k>n_features must keep all (warn), not error: {e:?}");
return;
}
};
assert_eq!(
fitted.selected_indices(),
&[0usize, 1, 2, 3][..],
"k=7 on 4 features -> clamp + keep all (sklearn warn+keep-all)"
);
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false,..) is the test-failure signal in the fit-Err arm"
)]
fn reaudit_e_mixed_tie_constant_distinct() {
let x: Array2<f64> = array![
[1., 1., 5., 1., 1.],
[2., 2., 5., 3., 9.],
[7., 7., 5., 7., 2.],
[8., 8., 5., 8., 6.]
];
let y: Array1<usize> = array![0, 0, 1, 1];
let f3 = match SelectKBest::<f64>::new(3, ScoreFunc::FClassif).fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "k=3 fit failed: {e:?}");
return;
}
};
assert_eq!(
f3.selected_indices(),
&[0usize, 1, 3][..],
"mixed k=3: the two tied cols (0,1) + the higher distinct (3); constant col 2 excluded"
);
let f4 = match SelectKBest::<f64>::new(4, ScoreFunc::FClassif).fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "k=4 fit failed: {e:?}");
return;
}
};
assert_eq!(
f4.selected_indices(),
&[0usize, 1, 3, 4][..],
"mixed k=4: adds the lower distinct col 4; constant col 2 still last"
);
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false,..) is the test-failure signal in the fit-Err arm"
)]
fn reaudit_f_realish_8x4_3classes_k2() {
let x: Array2<f64> = array![
[5.1, 3.5, 1.4, 0.2],
[4.9, 3.0, 1.4, 0.2],
[6.2, 2.2, 4.5, 1.5],
[5.9, 3.2, 4.8, 1.8],
[7.3, 2.9, 6.3, 1.8],
[6.7, 3.3, 5.7, 2.5],
[5.0, 3.4, 1.5, 0.2],
[6.4, 3.2, 4.5, 1.5]
];
let y: Array1<usize> = array![0, 0, 1, 1, 2, 2, 0, 1];
let sel = SelectKBest::<f64>::new(2, ScoreFunc::FClassif);
let fitted = match sel.fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "fit failed: {e:?}");
return;
}
};
let scores = fitted.scores();
let expected = [
38.663_903_061_219_145,
0.795_347_744_360_936_9,
287.757_601_351_380_97,
43.309_426_229_507_73,
];
for (j, &want) in expected.iter().enumerate() {
assert!(
(scores[j] - want).abs() < 1e-9,
"col{j} F={} expected {want}",
scores[j]
);
}
assert_eq!(
fitted.selected_indices(),
&[2usize, 3][..],
"realish k=2 -> the two highest-F cols [2,3]"
);
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false,..) is the test-failure signal in the fit-Err arm"
)]
fn reaudit_g_scores_nan_at_constant_finite_elsewhere() {
let x: Array2<f64> = array![
[1., 1., 5., 1., 1.],
[2., 2., 5., 3., 9.],
[7., 7., 5., 7., 2.],
[8., 8., 5., 8., 6.]
];
let y: Array1<usize> = array![0, 0, 1, 1];
let sel = SelectKBest::<f64>::new(3, ScoreFunc::FClassif);
let fitted = match sel.fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "fit failed: {e:?}");
return;
}
};
let s = fitted.scores();
assert!(
s[2].is_nan(),
"constant col 2 score must be NaN (matches sklearn scores_[2]), got {}",
s[2]
);
let finite = [(0usize, 72.0), (1, 72.0), (3, 24.2), (4, 0.05)];
for (j, want) in finite {
assert!(
(s[j] - want).abs() < 1e-9,
"col{j} F={} expected {want}",
s[j]
);
}
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false,..) is the test-failure signal in the fit-Err arm"
)]
fn reaudit_h_f32_tie_k2_keeps_two_highest() {
let x: Array2<f32> = array![
[1.0f32, 1.0, 1.0, 1.0],
[2.0, 2.0, 2.0, 2.0],
[7.0, 7.0, 7.0, 7.0],
[8.0, 8.0, 8.0, 8.0]
];
let y: Array1<usize> = array![0, 0, 1, 1];
let sel = SelectKBest::<f32>::new(2, ScoreFunc::FClassif);
let fitted = match sel.fit(&x, &y) {
Ok(f) => f,
Err(e) => {
assert!(false, "f32 fit failed: {e:?}");
return;
}
};
assert_eq!(
fitted.selected_indices(),
&[2usize, 3][..],
"f32 4-way tie k=2 must keep the two HIGHEST indices"
);
}