use ferrolearn_core::error::FerroError;
use ferrolearn_core::traits::Transform;
use ferrolearn_preprocess::sequential_feature_selector::{Direction, SequentialFeatureSelector};
use ndarray::{Array1, Array2, array};
fn mean_sum_score(x: &Array2<f64>, _y: &Array1<f64>) -> Result<f64, FerroError> {
let score: f64 = x
.columns()
.into_iter()
.map(|c| c.sum() / c.len() as f64)
.sum();
Ok(score)
}
#[test]
fn divergence_n_features_to_select_equals_n_features() {
let x = array![
[1.0, 10.0, 0.1],
[2.0, 20.0, 0.2],
[3.0, 30.0, 0.3],
[4.0, 40.0, 0.4]
];
let y = array![1.0, 2.0, 3.0, 4.0];
let sfs = SequentialFeatureSelector::new(3, Direction::Forward);
let result = sfs.fit(&x, &y, mean_sum_score);
assert!(
result.is_err(),
"sklearn (_sequential.py:227-228) raises \
ValueError(\"n_features_to_select must be < n_features.\") when \
n_features_to_select (3) == n_features (3); ferrolearn must Err but returned Ok"
);
}
#[test]
fn divergence_ensure_min_features_two() {
let x = array![[1.0], [2.0], [3.0], [4.0]];
let y = array![1.0, 2.0, 3.0, 4.0];
let sfs = SequentialFeatureSelector::new(1, Direction::Forward);
let result = sfs.fit(&x, &y, mean_sum_score);
assert!(
result.is_err(),
"sklearn (_sequential.py:214, ensure_min_features=2) raises ValueError \
for a 1-feature X (minimum of 2 required); ferrolearn must Err but returned Ok"
);
}
#[test]
fn green_forward_n1_picks_highest_mean() {
let x = array![
[1.0, 10.0, 0.1],
[2.0, 20.0, 0.2],
[3.0, 30.0, 0.3],
[4.0, 40.0, 0.4]
];
let y = array![1.0, 2.0, 3.0, 4.0];
let fitted = SequentialFeatureSelector::new(1, Direction::Forward)
.fit(&x, &y, mean_sum_score)
.expect("forward fit must succeed");
assert_eq!(fitted.selected_indices(), &[1]);
}
#[test]
fn green_forward_n2_picks_top_two() {
let x = array![
[1.0, 10.0, 0.1],
[2.0, 20.0, 0.2],
[3.0, 30.0, 0.3],
[4.0, 40.0, 0.4]
];
let y = array![1.0, 2.0, 3.0, 4.0];
let fitted = SequentialFeatureSelector::new(2, Direction::Forward)
.fit(&x, &y, mean_sum_score)
.expect("forward fit must succeed");
assert_eq!(fitted.selected_indices(), &[0, 1]);
}
#[test]
fn green_backward_n1_keeps_highest_mean() {
let x = array![
[1.0, 10.0, 0.1],
[2.0, 20.0, 0.2],
[3.0, 30.0, 0.3],
[4.0, 40.0, 0.4]
];
let y = array![1.0, 2.0, 3.0, 4.0];
let fitted = SequentialFeatureSelector::new(1, Direction::Backward)
.fit(&x, &y, mean_sum_score)
.expect("backward fit must succeed");
assert_eq!(fitted.selected_indices(), &[1]);
}
#[test]
fn green_backward_n2_drops_lowest() {
let x = array![
[1.0, 10.0, 0.1],
[2.0, 20.0, 0.2],
[3.0, 30.0, 0.3],
[4.0, 40.0, 0.4]
];
let y = array![1.0, 2.0, 3.0, 4.0];
let fitted = SequentialFeatureSelector::new(2, Direction::Backward)
.fit(&x, &y, mean_sum_score)
.expect("backward fit must succeed");
assert_eq!(fitted.selected_indices(), &[0, 1]);
}
#[test]
fn green_forward_tie_break_lowest_index() {
let x = array![[5.0, 5.0, 1.0], [5.0, 5.0, 1.0]];
let y = array![1.0, 2.0];
let fitted = SequentialFeatureSelector::new(1, Direction::Forward)
.fit(&x, &y, mean_sum_score)
.expect("forward fit must succeed");
assert_eq!(fitted.selected_indices(), &[0]);
}
#[test]
fn green_backward_tie_break_removal() {
let x = array![[5.0, 5.0, 1.0], [5.0, 5.0, 1.0]];
let y = array![1.0, 2.0];
let fitted = SequentialFeatureSelector::new(1, Direction::Backward)
.fit(&x, &y, mean_sum_score)
.expect("backward fit must succeed");
assert_eq!(fitted.selected_indices(), &[1]);
}
#[test]
fn green_zero_features_err() {
let x = array![[1.0, 2.0], [3.0, 4.0]];
let y = array![1.0, 2.0];
let sfs = SequentialFeatureSelector::new(0, Direction::Forward);
assert!(sfs.fit(&x, &y, mean_sum_score).is_err());
}
#[test]
fn green_zero_rows_err() {
let x: Array2<f64> = Array2::zeros((0, 3));
let y: Array1<f64> = Array1::zeros(0);
let sfs = SequentialFeatureSelector::new(1, Direction::Forward);
assert!(sfs.fit(&x, &y, mean_sum_score).is_err());
}
#[test]
fn green_y_length_mismatch_err() {
let x = array![[1.0, 2.0], [3.0, 4.0]];
let y = array![1.0];
let sfs = SequentialFeatureSelector::new(1, Direction::Forward);
assert!(sfs.fit(&x, &y, mean_sum_score).is_err());
}
#[test]
fn green_transform_ncols_mismatch_err() {
let x = array![[1.0, 2.0], [3.0, 4.0]];
let y = array![1.0, 2.0];
let fitted = SequentialFeatureSelector::new(1, Direction::Forward)
.fit(&x, &y, mean_sum_score)
.expect("fit must succeed");
let x_bad = array![[1.0, 2.0, 3.0]];
assert!(fitted.transform(&x_bad).is_err());
}
#[test]
fn green_score_fn_error_propagated() {
let x = array![[1.0, 2.0], [3.0, 4.0]];
let y = array![1.0, 2.0];
let bad_fn = |_x: &Array2<f64>, _y: &Array1<f64>| -> Result<f64, FerroError> {
Err(FerroError::NumericalInstability {
message: "test error".into(),
})
};
let sfs = SequentialFeatureSelector::new(1, Direction::Forward);
assert!(sfs.fit(&x, &y, bad_fn).is_err());
}
#[test]
fn reaudit_equals_nfeatures_4feat() {
let x = array![
[1.0, 2.0, 3.0, 4.0],
[5.0, 6.0, 7.0, 8.0],
[9.0, 10.0, 11.0, 12.0]
];
let y = array![1.0, 2.0, 3.0];
let sfs = SequentialFeatureSelector::new(4, Direction::Forward);
assert!(sfs.fit(&x, &y, mean_sum_score).is_err());
}
#[test]
fn reaudit_equals_nfeatures_2feat() {
let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
let y = array![1.0, 2.0, 3.0];
let sfs = SequentialFeatureSelector::new(2, Direction::Forward);
assert!(sfs.fit(&x, &y, mean_sum_score).is_err());
}
#[test]
fn reaudit_greater_than_nfeatures() {
let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
let y = array![1.0, 2.0, 3.0];
let sfs = SequentialFeatureSelector::new(5, Direction::Forward);
assert!(sfs.fit(&x, &y, mean_sum_score).is_err());
}
#[test]
fn reaudit_valid_count_2_of_4_ok() {
let x = array![
[1.0, 2.0, 3.0, 4.0],
[5.0, 6.0, 7.0, 8.0],
[9.0, 10.0, 11.0, 12.0]
];
let y = array![1.0, 2.0, 3.0];
let sfs = SequentialFeatureSelector::new(2, Direction::Forward);
let fitted = sfs
.fit(&x, &y, mean_sum_score)
.expect("valid count 2<4 must succeed");
assert_eq!(fitted.n_features_selected(), 2);
}
#[test]
fn reaudit_one_feature_min_features_message() {
let x = array![[1.0], [2.0], [3.0]];
let y = array![1.0, 2.0, 3.0];
let sfs = SequentialFeatureSelector::new(3, Direction::Forward);
match sfs.fit(&x, &y, mean_sum_score) {
Err(FerroError::InvalidParameter { reason, .. }) => {
assert!(
reason.contains("minimum of 2"),
"expected ensure_min_features message, got: {reason}"
);
}
other => panic!("expected InvalidParameter min-features error, got {other:?}"),
}
}
#[test]
fn reaudit_precedence_min_features_before_count() {
let x = array![[1.0], [2.0], [3.0]];
let y = array![1.0, 2.0, 3.0];
let sfs = SequentialFeatureSelector::new(5, Direction::Forward);
match sfs.fit(&x, &y, mean_sum_score) {
Err(FerroError::InvalidParameter { reason, .. }) => {
assert!(
reason.contains("minimum of 2"),
"precedence: min-features must fire before count; got: {reason}"
);
assert!(
!reason.contains("must be <"),
"precedence: must NOT be the count error; got: {reason}"
);
}
other => panic!("expected InvalidParameter min-features error, got {other:?}"),
}
}
#[test]
fn reaudit_zero_feature_min_features() {
let x: Array2<f64> = Array2::zeros((3, 0));
let y = array![1.0, 2.0, 3.0];
let sfs = SequentialFeatureSelector::new(1, Direction::Forward);
match sfs.fit(&x, &y, mean_sum_score) {
Err(FerroError::InvalidParameter { reason, .. }) => {
assert!(reason.contains("minimum of 2"), "got: {reason}");
}
other => panic!("expected InvalidParameter min-features error, got {other:?}"),
}
}