use ferrolearn_core::error::FerroError;
use ferrolearn_core::traits::{Fit, Transform};
use ferrolearn_preprocess::select_from_model::{SelectFromModelExt, ThresholdStrategy};
use ndarray::{Array1, Array2, array};
#[test]
fn divergence_mean_threshold() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Mean, None);
let importances = array![0.1, 0.5, 0.4];
match sel.fit(&importances, &()) {
Ok(fitted) => assert_eq!(fitted.selected_indices(), &[1, 2]),
Err(e) => report_unexpected_err(&e),
}
}
#[test]
fn divergence_median_threshold_odd() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Median, None);
let importances = array![0.1, 0.5, 0.3];
match sel.fit(&importances, &()) {
Ok(fitted) => assert_eq!(fitted.selected_indices(), &[1, 2]),
Err(e) => report_unexpected_err(&e),
}
}
#[test]
fn divergence_median_threshold_even() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Median, None);
let importances = array![0.1, 0.5, 0.2, 0.6];
match sel.fit(&importances, &()) {
Ok(fitted) => {
let expected_median = (0.2_f64 + 0.5_f64) / 2.0;
assert!(
(fitted.threshold_value() - expected_median).abs() < 1e-15,
"median threshold {} != np.median 0.35",
fitted.threshold_value()
);
assert_eq!(fitted.selected_indices(), &[1, 3]);
}
Err(e) => report_unexpected_err(&e),
}
}
#[test]
fn divergence_value_threshold() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Value(0.45), None);
let importances = array![0.1, 0.5, 0.4];
match sel.fit(&importances, &()) {
Ok(fitted) => assert_eq!(fitted.selected_indices(), &[1]),
Err(e) => report_unexpected_err(&e),
}
}
#[test]
fn divergence_max_features_no_pressure() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Value(0.0), Some(2));
let importances = array![0.3, 0.5, 0.1, 0.7];
match sel.fit(&importances, &()) {
Ok(fitted) => assert_eq!(fitted.selected_indices(), &[1, 3]),
Err(e) => report_unexpected_err(&e),
}
}
#[test]
fn divergence_max_features_with_threshold_pressure() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Value(0.85), Some(2));
let importances = array![0.1, 0.9, 0.8];
match sel.fit(&importances, &()) {
Ok(fitted) => assert_eq!(fitted.selected_indices(), &[1]),
Err(e) => report_unexpected_err(&e),
}
}
#[test]
fn divergence_max_features_tie_break() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Value(0.0), Some(2));
let importances = array![0.5, 0.5, 0.5, 0.1];
match sel.fit(&importances, &()) {
Ok(fitted) => assert_eq!(fitted.selected_indices(), &[0, 1]),
Err(e) => report_unexpected_err(&e),
}
}
#[test]
fn divergence_threshold_boundary_inclusive() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Value(0.4), None);
let importances = array![0.1, 0.5, 0.4];
match sel.fit(&importances, &()) {
Ok(fitted) => assert_eq!(fitted.selected_indices(), &[1, 2]),
Err(e) => report_unexpected_err(&e),
}
}
#[test]
fn divergence_f32_mean() {
let sel = SelectFromModelExt::<f32>::new(ThresholdStrategy::Mean, None);
let importances: Array1<f32> = array![0.1f32, 0.5, 0.4];
match sel.fit(&importances, &()) {
Ok(fitted) => assert_eq!(fitted.selected_indices(), &[1, 2]),
Err(e) => report_unexpected_err(&e),
}
}
#[test]
fn divergence_transform_projects_selected_columns() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Value(0.4), None);
let importances = array![0.1, 0.5, 0.4];
match sel.fit(&importances, &()) {
Ok(fitted) => {
let x: Array2<f64> = array![[10.0, 11.0, 12.0], [20.0, 21.0, 22.0]];
match fitted.transform(&x) {
Ok(out) => {
assert_eq!(out.shape(), &[2, 2]);
assert_eq!(out, array![[11.0, 12.0], [21.0, 22.0]]);
}
Err(e) => report_unexpected_err(&e),
}
}
Err(e) => report_unexpected_err(&e),
}
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
fn divergence_empty_importances_err() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Mean, None);
let importances: Array1<f64> = Array1::zeros(0);
match sel.fit(&importances, &()) {
Ok(_) => assert!(false, "expected Err on empty importance vector"),
Err(FerroError::InvalidParameter { .. }) => {}
Err(other) => assert!(false, "expected InvalidParameter, got {other:?}"),
}
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
fn divergence_percentile_zero_err() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Percentile(0.0), None);
let importances = array![0.1, 0.5, 0.3];
match sel.fit(&importances, &()) {
Ok(_) => assert!(false, "expected Err on Percentile(0.0)"),
Err(FerroError::InvalidParameter { .. }) => {}
Err(other) => assert!(false, "expected InvalidParameter, got {other:?}"),
}
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
fn divergence_percentile_over_100_err() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Percentile(101.0), None);
let importances = array![0.1, 0.5, 0.3];
match sel.fit(&importances, &()) {
Ok(_) => assert!(false, "expected Err on Percentile(101.0)"),
Err(FerroError::InvalidParameter { .. }) => {}
Err(other) => assert!(false, "expected InvalidParameter, got {other:?}"),
}
}
#[test]
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
fn divergence_transform_ncols_mismatch_err() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Mean, None);
let importances = array![0.5, 0.5];
match sel.fit(&importances, &()) {
Ok(fitted) => {
let x_bad: Array2<f64> = array![[1.0, 2.0, 3.0]]; match fitted.transform(&x_bad) {
Ok(_) => assert!(false, "expected ShapeMismatch on ncols mismatch"),
Err(FerroError::ShapeMismatch { .. }) => {}
Err(other) => assert!(false, "expected ShapeMismatch, got {other:?}"),
}
}
Err(e) => report_unexpected_err(&e),
}
}
#[test]
fn extension_percentile_100_keeps_all() {
let sel = SelectFromModelExt::<f64>::new(ThresholdStrategy::Percentile(100.0), None);
let importances = array![0.1, 0.5, 0.3];
match sel.fit(&importances, &()) {
Ok(fitted) => assert_eq!(fitted.n_features_selected(), 3),
Err(e) => report_unexpected_err(&e),
}
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
fn report_unexpected_err(e: &FerroError) {
assert!(false, "unexpected Err from fit/transform: {e:?}");
}