use ferrolearn_core::traits::{Fit, FitTransform, Transform};
use ferrolearn_preprocess::imputer::{ImputeStrategy, SimpleImputer};
use ndarray::{Array2, array};
#[test]
fn divergence_mean_all_nan_column_dropped() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Mean);
let x = array![[1.0, f64::NAN], [3.0, f64::NAN], [5.0, f64::NAN]];
match imputer.fit_transform(&x) {
Ok(out) => {
assert_eq!(
out.ncols(),
1,
"sklearn drops all-NaN column (shape (3,1)); ferrolearn kept it"
);
assert_eq!(out.nrows(), 3);
assert_eq!(out[[0, 0]], 1.0);
assert_eq!(out[[1, 0]], 3.0);
assert_eq!(out[[2, 0]], 5.0);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit_transform unexpectedly errored: {e}"),
}
}
#[test]
fn divergence_median_all_nan_column_dropped() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Median);
let x = array![[1.0, f64::NAN], [3.0, f64::NAN], [5.0, f64::NAN]];
match imputer.fit_transform(&x) {
Ok(out) => {
assert_eq!(
out.ncols(),
1,
"sklearn drops all-NaN column (shape (3,1)); ferrolearn kept it"
);
assert_eq!(out.nrows(), 3);
assert_eq!(out[[0, 0]], 1.0);
assert_eq!(out[[1, 0]], 3.0);
assert_eq!(out[[2, 0]], 5.0);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit_transform unexpectedly errored: {e}"),
}
}
#[test]
fn divergence_median_multi_feature_one_all_nan_dropped() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Median);
let x = array![[1.0, f64::NAN], [f64::NAN, f64::NAN], [5.0, f64::NAN]];
match imputer.fit_transform(&x) {
Ok(out) => {
assert_eq!(
out.ncols(),
1,
"sklearn drops the all-NaN column, keeps the median-imputed one (shape (3,1))"
);
assert_eq!(out.nrows(), 3);
assert_eq!(out[[0, 0]], 1.0);
assert_eq!(out[[1, 0]], 3.0);
assert_eq!(out[[2, 0]], 5.0);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit_transform unexpectedly errored: {e}"),
}
}
#[test]
fn green_constant_all_nan_column_kept() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Constant(-99.0));
let x = array![[1.0, f64::NAN], [f64::NAN, 4.0]];
match imputer.fit_transform(&x) {
Ok(out) => {
assert_eq!(out.ncols(), 2, "constant strategy keeps all columns");
assert_eq!(out.nrows(), 2);
assert_eq!(out[[0, 0]], 1.0);
assert_eq!(out[[0, 1]], -99.0);
assert_eq!(out[[1, 0]], -99.0);
assert_eq!(out[[1, 1]], 4.0);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit_transform unexpectedly errored: {e}"),
}
}
#[test]
fn green_mean_fill_values() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Mean);
let x = array![[1.0, f64::NAN], [3.0, 4.0], [5.0, 6.0]];
match imputer.fit(&x, &()) {
Ok(fitted) => {
let f = fitted.fill_values();
assert!((f[0] - 3.0).abs() < 1e-12, "col0 mean: {} != 3.0", f[0]);
assert!((f[1] - 5.0).abs() < 1e-12, "col1 mean: {} != 5.0", f[1]);
match fitted.transform(&x) {
Ok(out) => {
assert!((out[[0, 1]] - 5.0).abs() < 1e-12, "imputed NaN -> 5.0");
assert!((out[[1, 1]] - 4.0).abs() < 1e-12, "untouched 4.0");
assert!((out[[2, 1]] - 6.0).abs() < 1e-12, "untouched 6.0");
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "transform errored: {e}"),
}
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
}
#[test]
fn green_median_fill_values() {
let probe = |x: &Array2<f64>, expected: f64| {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Median);
match imputer.fit(x, &()) {
Ok(fitted) => {
let got = fitted.fill_values()[0];
assert!((got - expected).abs() < 1e-12, "median {got} != {expected}");
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
};
probe(&array![[1.0], [3.0], [5.0], [7.0], [9.0]], 5.0);
probe(&array![[1.0], [3.0], [5.0], [7.0]], 4.0);
probe(&array![[2.0], [f64::NAN], [4.0], [6.0]], 4.0);
}
#[test]
fn green_median_even_count_adversarial() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Median);
let x = array![[1.0], [2.0], [3.0], [100.0]];
match imputer.fit(&x, &()) {
Ok(fitted) => {
let got = fitted.fill_values()[0];
assert!(
(got - 2.5).abs() < 1e-12,
"even-count median {got} != 2.5 (np.ma.median averages 2 and 3)"
);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
}
#[test]
fn green_most_frequent_fill_values() {
let probe = |x: &Array2<f64>, expected: f64| {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::MostFrequent);
match imputer.fit(x, &()) {
Ok(fitted) => {
let got = fitted.fill_values()[0];
assert!(
(got - expected).abs() < 1e-12,
"most_frequent {got} != {expected}"
);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
};
probe(&array![[1.0], [2.0], [2.0], [3.0]], 2.0);
probe(&array![[1.0], [1.0], [3.0], [3.0]], 1.0);
probe(&array![[1.0], [f64::NAN], [2.0], [2.0]], 2.0);
}
#[test]
fn green_f32_mean() {
let imputer = SimpleImputer::<f32>::new(ImputeStrategy::Mean);
let x: Array2<f32> = array![[1.0f32, f32::NAN], [3.0, 4.0]];
match imputer.fit(&x, &()) {
Ok(fitted) => match fitted.transform(&x) {
Ok(out) => assert!((out[[0, 1]] - 4.0f32).abs() < 1e-6, "f32 imputed -> 4.0"),
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "transform errored: {e}"),
},
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
}
#[test]
fn green_fit_zero_rows_errors() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Mean);
let x: Array2<f64> = Array2::zeros((0, 3));
assert!(imputer.fit(&x, &()).is_err(), "zero-row fit must error");
}
#[test]
fn green_transform_ncols_mismatch_errors() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Mean);
let x_train = array![[1.0, 2.0], [3.0, 4.0]];
match imputer.fit(&x_train, &()) {
Ok(fitted) => {
let x_bad = array![[1.0, 2.0, 3.0]];
assert!(
fitted.transform(&x_bad).is_err(),
"ncols mismatch must error"
);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
}
#[test]
fn green_unfitted_transform_errors() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Mean);
let x = array![[1.0, 2.0]];
assert!(
imputer.transform(&x).is_err(),
"unfitted transform must error"
);
}
#[test]
fn reaudit_a_column_order_two_all_nan_dropped() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Mean);
let x = array![
[1.0, f64::NAN, 7.0, f64::NAN],
[3.0, f64::NAN, 9.0, f64::NAN],
[5.0, f64::NAN, 11.0, f64::NAN]
];
match imputer.fit(&x, &()) {
Ok(fitted) => {
assert_eq!(
fitted.kept_indices(),
&[0usize, 2usize],
"kept_indices must preserve {{0,2}} order"
);
match fitted.transform(&x) {
Ok(out) => {
assert_eq!(out.nrows(), 3);
assert_eq!(out.ncols(), 2, "sklearn out.shape=(3,2)");
assert!((out[[0, 0]] - 1.0).abs() < 1e-9);
assert!((out[[1, 0]] - 3.0).abs() < 1e-9);
assert!((out[[2, 0]] - 5.0).abs() < 1e-9);
assert!((out[[0, 1]] - 7.0).abs() < 1e-9);
assert!((out[[1, 1]] - 9.0).abs() < 1e-9);
assert!((out[[2, 1]] - 11.0).abs() < 1e-9);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "transform errored: {e}"),
}
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
}
#[test]
fn reaudit_b_all_columns_all_nan_zero_output() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Mean);
let x = array![[f64::NAN, f64::NAN], [f64::NAN, f64::NAN]];
match imputer.fit(&x, &()) {
Ok(fitted) => {
assert!(fitted.kept_indices().is_empty(), "no column survives");
assert!(fitted.fill_values()[0].is_nan());
assert!(fitted.fill_values()[1].is_nan());
match fitted.transform(&x) {
Ok(out) => {
assert_eq!(out.ncols(), 0, "sklearn out.shape=(2,0)");
assert_eq!(out.nrows(), 2);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "transform errored: {e}"),
}
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
}
#[test]
fn reaudit_c_most_frequent_all_nan_dropped() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::MostFrequent);
let x = array![[1.0, f64::NAN], [1.0, f64::NAN], [2.0, f64::NAN]];
match imputer.fit(&x, &()) {
Ok(fitted) => {
assert_eq!(fitted.kept_indices(), &[0usize]);
assert!((fitted.fill_values()[0] - 1.0).abs() < 1e-9);
assert!(fitted.fill_values()[1].is_nan());
match fitted.transform(&x) {
Ok(out) => {
assert_eq!(out.ncols(), 1, "sklearn out.shape=(3,1)");
assert_eq!(out.nrows(), 3);
assert!((out[[0, 0]] - 1.0).abs() < 1e-9);
assert!((out[[1, 0]] - 1.0).abs() < 1e-9);
assert!((out[[2, 0]] - 2.0).abs() < 1e-9);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "transform errored: {e}"),
}
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
}
#[test]
fn reaudit_d_constant_all_nan_kept_filled_constant() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Constant(-7.0));
let x = array![[1.0, f64::NAN], [f64::NAN, f64::NAN]];
match imputer.fit(&x, &()) {
Ok(fitted) => {
assert_eq!(
fitted.kept_indices(),
&[0usize, 1usize],
"constant keeps all"
);
assert!((fitted.fill_values()[0] - (-7.0)).abs() < 1e-9);
assert!((fitted.fill_values()[1] - (-7.0)).abs() < 1e-9);
match fitted.transform(&x) {
Ok(out) => {
assert_eq!(out.ncols(), 2, "sklearn out.shape=(2,2)");
assert_eq!(out.nrows(), 2);
assert!((out[[0, 0]] - 1.0).abs() < 1e-9);
assert!((out[[0, 1]] - (-7.0)).abs() < 1e-9);
assert!((out[[1, 0]] - (-7.0)).abs() < 1e-9);
assert!(
(out[[1, 1]] - (-7.0)).abs() < 1e-9,
"all-NaN col filled CONSTANT not 0"
);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "transform errored: {e}"),
}
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
}
#[test]
fn reaudit_e_fill_values_mirror_statistics_nan() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Median);
let x = array![[1.0, f64::NAN], [f64::NAN, f64::NAN], [5.0, f64::NAN]];
match imputer.fit(&x, &()) {
Ok(fitted) => {
let f = fitted.fill_values();
assert_eq!(f.len(), 2, "fill_values has one entry per INPUT column");
assert!((f[0] - 3.0).abs() < 1e-9, "col0 median=3.0");
assert!(f[1].is_nan(), "dropped col1 statistics_ is NaN");
assert_eq!(fitted.kept_indices(), &[0usize]);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
}
#[test]
fn reaudit_f_transform_separate_matrix_projection() {
let imputer = SimpleImputer::<f64>::new(ImputeStrategy::Mean);
let x_fit = array![
[1.0, f64::NAN, 7.0],
[3.0, f64::NAN, 9.0],
[5.0, f64::NAN, 11.0]
];
match imputer.fit(&x_fit, &()) {
Ok(fitted) => {
assert_eq!(fitted.kept_indices(), &[0usize, 2usize]);
let x_new = array![[f64::NAN, 100.0, f64::NAN], [10.0, 200.0, 20.0]];
match fitted.transform(&x_new) {
Ok(out) => {
assert_eq!(out.ncols(), 2, "sklearn out.shape=(2,2)");
assert_eq!(out.nrows(), 2);
assert!(
(out[[0, 0]] - 3.0).abs() < 1e-9,
"NaN in kept col0 -> mean 3"
);
assert!((out[[1, 0]] - 10.0).abs() < 1e-9);
assert!(
(out[[0, 1]] - 9.0).abs() < 1e-9,
"NaN in kept col2 -> mean 9"
);
assert!((out[[1, 1]] - 20.0).abs() < 1e-9);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "transform errored: {e}"),
}
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
}
#[test]
fn reaudit_g_f32_all_nan_column_dropped() {
let imputer = SimpleImputer::<f32>::new(ImputeStrategy::Mean);
let x: Array2<f32> = array![[1.0f32, f32::NAN, 7.0], [3.0, f32::NAN, 9.0]];
match imputer.fit(&x, &()) {
Ok(fitted) => {
assert_eq!(fitted.kept_indices(), &[0usize, 2usize]);
assert!(
(fitted.fill_values()[0] - 2.0f32).abs() < 1e-6,
"col0 mean=2"
);
assert!(fitted.fill_values()[1].is_nan(), "dropped col1 -> NaN");
assert!(
(fitted.fill_values()[2] - 8.0f32).abs() < 1e-6,
"col2 mean=8"
);
match fitted.transform(&x) {
Ok(out) => {
assert_eq!(out.ncols(), 2, "sklearn out.shape=(2,2)");
assert_eq!(out.nrows(), 2);
assert!((out[[0, 0]] - 1.0f32).abs() < 1e-6);
assert!((out[[1, 0]] - 3.0f32).abs() < 1e-6);
assert!((out[[0, 1]] - 7.0f32).abs() < 1e-6);
assert!((out[[1, 1]] - 9.0f32).abs() < 1e-6);
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "transform errored: {e}"),
}
}
#[allow(
clippy::assertions_on_constants,
reason = "error arm fails loudly without panic!/unwrap (anti-pattern gate)"
)]
Err(e) => assert!(false, "fit errored: {e}"),
}
}