use ferrolearn_core::traits::{Fit, FitTransform, Transform};
use ferrolearn_preprocess::{HandleUnknown, OrdinalEncoder};
use ndarray::Array2;
fn cats(lists: &[&[&str]]) -> Vec<Vec<String>> {
lists
.iter()
.map(|l| l.iter().map(std::string::ToString::to_string).collect())
.collect()
}
fn make_2col(rows: &[(&str, &str)]) -> Array2<String> {
let flat: Vec<String> = rows
.iter()
.flat_map(|(a, b)| [a.to_string(), b.to_string()])
.collect();
Array2::from_shape_vec((rows.len(), 2), flat).unwrap()
}
fn make_1col(vals: &[&str]) -> Array2<String> {
Array2::from_shape_vec(
(vals.len(), 1),
vals.iter().map(std::string::ToString::to_string).collect(),
)
.unwrap()
}
#[test]
fn green_value_match_and_categories() {
let sk_values: [[f64; 2]; 4] = [[1., 2.], [2., 0.], [1., 1.], [0., 2.]];
let sk_cat0 = ["bird", "cat", "dog"];
let sk_cat1 = ["large", "medium", "small"];
let enc = OrdinalEncoder::new();
let x = make_2col(&[
("cat", "small"),
("dog", "large"),
("cat", "medium"),
("bird", "small"),
]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(fitted.categories()[0], sk_cat0, "categories_[0] (col 0)");
assert_eq!(fitted.categories()[1], sk_cat1, "categories_[1] (col 1)");
let encoded: ndarray::Array2<f64> = fitted.transform(&x).unwrap();
for (i, row) in sk_values.iter().enumerate() {
for (j, &expect) in row.iter().enumerate() {
assert_eq!(
encoded[[i, j]],
expect,
"ordinal value at [{i},{j}] vs sklearn float64 oracle"
);
}
}
}
#[test]
fn green_lexicographic_sort_matches_np_unique() {
let sk_sorted = ["10", "2", "A", "B", "a", "b"];
let enc = OrdinalEncoder::new();
let x = make_1col(&["B", "a", "A", "b", "10", "2"]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(
fitted.categories()[0],
sk_sorted,
"categories_[0] sort order vs np.unique"
);
}
#[test]
fn green_non_ascii_codepoint_order() {
let sk_cats = ["Z", "a", "z", "ä", "é", "€"];
let sk_tf: [f64; 3] = [5., 0., 1.];
let enc = OrdinalEncoder::new();
let x = make_1col(&["z", "é", "a", "Z", "€", "ä"]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(
fitted.categories()[0],
sk_cats,
"non-ASCII categories_ vs live OrdinalEncoder oracle"
);
let probe = make_1col(&["€", "Z", "a"]);
let out = fitted.transform(&probe).unwrap();
for (i, &expect) in sk_tf.iter().enumerate() {
assert_eq!(out[[i, 0]], expect, "non-ASCII ordinal at [{i},0]");
}
}
#[test]
fn green_unknown_category_rejected() {
let enc = OrdinalEncoder::new();
let x_train = make_2col(&[("cat", "small"), ("dog", "large")]);
let fitted = enc.fit(&x_train, &()).unwrap();
let x_test = make_2col(&[("fish", "small")]);
assert!(
fitted.transform(&x_test).is_err(),
"sklearn raises ValueError on unknown 'fish'; ferrolearn must Err too"
);
}
#[test]
fn green_empty_fit_rejected_matches_sklearn() {
let enc = OrdinalEncoder::new();
let x: Array2<String> = Array2::from_shape_vec((0, 2), vec![]).unwrap();
assert!(
enc.fit(&x, &()).is_err(),
"sklearn raises ValueError on 0-sample fit; ferrolearn must Err too"
);
}
#[test]
fn green_fit_transform_equals_oracle() {
let sk_values: [[f64; 2]; 4] = [[1., 2.], [2., 0.], [1., 1.], [0., 2.]];
let expected: Array2<f64> = Array2::from_shape_vec(
(4, 2),
sk_values.iter().flat_map(|r| r.iter().copied()).collect(),
)
.unwrap();
let enc = OrdinalEncoder::new();
let x = make_2col(&[
("cat", "small"),
("dog", "large"),
("cat", "medium"),
("bird", "small"),
]);
let via_ft = enc.fit_transform(&x).unwrap();
let via_sep = enc.fit(&x, &()).unwrap().transform(&x).unwrap();
assert_eq!(via_ft, expected, "fit_transform vs live oracle values");
assert_eq!(via_sep, expected, "fit+transform vs live oracle values");
assert_eq!(via_ft, via_sep, "fit_transform == fit then transform");
}
#[test]
fn green_duplicates_independence_single_row() {
let enc = OrdinalEncoder::new();
let dup = make_1col(&["a", "a", "b", "a"]);
let fitted = enc.fit(&dup, &()).unwrap();
assert_eq!(fitted.categories()[0], ["a", "b"], "dup categories_");
let indep = make_2col(&[("a", "b"), ("b", "a")]);
let fitted = enc.fit(&indep, &()).unwrap();
assert_eq!(fitted.categories()[0], ["a", "b"], "indep col0");
assert_eq!(fitted.categories()[1], ["a", "b"], "indep col1");
let out = fitted.transform(&indep).unwrap();
assert_eq!(out[[0, 0]], 0.0);
assert_eq!(out[[0, 1]], 1.0);
assert_eq!(out[[1, 0]], 1.0);
assert_eq!(out[[1, 1]], 0.0);
let single = make_2col(&[("solo", "x")]);
let fitted = enc.fit(&single, &()).unwrap();
assert_eq!(fitted.categories()[0], ["solo"], "single-row col0");
assert_eq!(fitted.categories()[1], ["x"], "single-row col1");
}
#[test]
fn green_fit_transform_f64_oracle() {
let sk: [[f64; 2]; 4] = [[0., 0.], [2., 2.], [1., 1.], [0., 2.]];
let enc = OrdinalEncoder::new();
let x = make_2col(&[
("bird", "large"),
("dog", "small"),
("cat", "medium"),
("bird", "small"),
]);
let encoded: Array2<f64> = enc.fit_transform(&x).unwrap();
assert_eq!(encoded.dim(), (4, 2), "shape vs oracle");
for (i, row) in sk.iter().enumerate() {
for (j, &expect) in row.iter().enumerate() {
assert_eq!(
encoded[[i, j]],
expect,
"f64 ordinal at [{i},{j}] vs oracle"
);
}
}
}
#[test]
fn green_exact_integer_index_to_f64() {
let cats = [
"b00", "b01", "b02", "b03", "b04", "b05", "b06", "b07", "b08", "b09", "b10",
];
let enc = OrdinalEncoder::new();
let x = make_1col(&cats);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(fitted.categories()[0][10], "b10", "lex index 10 is b10");
let probe = make_1col(&["b10"]);
let out: Array2<f64> = fitted.transform(&probe).unwrap();
assert_eq!(out[[0, 0]], 10.0, "index 10 -> exact 10.0 (lossless f64)");
}
#[test]
fn green_use_encoded_value_minus_one() {
let sk: [f64; 2] = [-1.0, 1.0];
let enc = OrdinalEncoder::new()
.with_handle_unknown(HandleUnknown::UseEncodedValue)
.with_unknown_value(-1.0);
let x_train = make_1col(&["cat", "dog", "cat"]);
let fitted = enc.fit(&x_train, &()).unwrap();
let probe = make_1col(&["bird", "dog"]);
let out: Array2<f64> = fitted.transform(&probe).unwrap();
assert_eq!(out[[0, 0]], sk[0], "unknown 'bird' -> -1.0 (oracle)");
assert_eq!(out[[1, 0]], sk[1], "seen 'dog' -> 1.0 (oracle)");
}
#[test]
fn green_use_encoded_value_multifeature() {
let sk: [[f64; 2]; 2] = [[-1.0, -1.0], [1.0, 0.0]];
let enc = OrdinalEncoder::new()
.with_handle_unknown(HandleUnknown::UseEncodedValue)
.with_unknown_value(-1.0);
let x_train = make_2col(&[("cat", "x"), ("dog", "y"), ("cat", "x")]);
let fitted = enc.fit(&x_train, &()).unwrap();
let probe = make_2col(&[("bird", "z"), ("dog", "x")]);
let out: Array2<f64> = fitted.transform(&probe).unwrap();
for (i, row) in sk.iter().enumerate() {
for (j, &expect) in row.iter().enumerate() {
assert_eq!(out[[i, j]], expect, "multi-feature uev at [{i},{j}]");
}
}
}
#[test]
fn green_use_encoded_value_nan() {
let enc = OrdinalEncoder::new()
.with_handle_unknown(HandleUnknown::UseEncodedValue)
.with_unknown_value(f64::NAN);
let x_train = make_1col(&["cat", "dog", "cat"]);
let fitted = enc.fit(&x_train, &()).unwrap();
let probe = make_1col(&["bird", "dog"]);
let out: Array2<f64> = fitted.transform(&probe).unwrap();
assert!(out[[0, 0]].is_nan(), "unknown 'bird' -> NaN (oracle nan)");
assert_eq!(out[[1, 0]], 1.0, "seen 'dog' -> 1.0 (oracle)");
}
#[test]
fn red_uev_requires_unknown_value() {
let enc = OrdinalEncoder::new().with_handle_unknown(HandleUnknown::UseEncodedValue);
let x_train = make_1col(&["cat", "dog", "cat"]);
assert!(
enc.fit(&x_train, &()).is_err(),
"sklearn raises TypeError (no unknown_value); ferrolearn must Err"
);
}
#[test]
fn red_error_mode_forbids_unknown_value() {
let enc = OrdinalEncoder::new().with_unknown_value(-1.0);
assert_eq!(
enc.handle_unknown(),
HandleUnknown::Error,
"default is Error"
);
let x_train = make_1col(&["cat", "dog", "cat"]);
assert!(
enc.fit(&x_train, &()).is_err(),
"sklearn raises TypeError (unknown_value in error mode); ferrolearn must Err"
);
}
#[test]
fn red_unknown_value_collision_in_range() {
let x_train = make_1col(&["cat", "dog", "cat"]);
let collide = OrdinalEncoder::new()
.with_handle_unknown(HandleUnknown::UseEncodedValue)
.with_unknown_value(1.0);
assert!(
collide.fit(&x_train, &()).is_err(),
"unknown_value=1 in [0,2) collides; sklearn ValueError -> Err"
);
}
#[test]
fn green_unknown_value_negative_or_oob_or_nan_ok() {
let x_train = make_1col(&["cat", "dog", "cat"]);
for v in [-1.0, 5.0, f64::NAN] {
let enc = OrdinalEncoder::new()
.with_handle_unknown(HandleUnknown::UseEncodedValue)
.with_unknown_value(v);
assert!(
enc.fit(&x_train, &()).is_ok(),
"unknown_value={v} is OK in sklearn (negative / out-of-range / nan)"
);
}
}
#[test]
fn green_error_mode_unknown_still_rejected() {
let enc = OrdinalEncoder::new(); assert_eq!(enc.handle_unknown(), HandleUnknown::Error);
assert_eq!(enc.unknown_value(), None);
let fitted = enc.fit(&make_1col(&["cat", "dog"]), &()).unwrap();
assert!(
fitted.transform(&make_1col(&["fish"])).is_err(),
"default error-mode must still reject unknown 'fish' (REQ-2 preserved)"
);
}
fn fit_inv_fixture() -> ferrolearn_preprocess::FittedOrdinalEncoder {
let enc = OrdinalEncoder::new();
let x = make_2col(&[("cat", "x"), ("dog", "y"), ("cat", "z")]);
enc.fit(&x, &()).unwrap()
}
#[test]
fn green_inverse_roundtrip_multifeature() {
let fitted = fit_inv_fixture();
let x = make_2col(&[("cat", "x"), ("dog", "y"), ("cat", "z")]);
let encoded = fitted.transform(&x).unwrap();
let recovered = fitted.inverse_transform(&encoded).unwrap();
assert_eq!(
recovered, x,
"inverse_transform(transform(X)) == X (oracle)"
);
}
#[test]
fn green_inverse_held_out_valid_ordinals() {
let fitted = fit_inv_fixture();
let probe = Array2::from_shape_vec((1, 2), vec![1.0_f64, 0.0]).unwrap();
let out = fitted.inverse_transform(&probe).unwrap();
assert_eq!(out[[0, 0]], "dog", "col0 index 1 -> 'dog' (oracle)");
assert_eq!(out[[0, 1]], "x", "col1 index 0 -> 'x' (oracle)");
}
#[test]
fn red_inverse_out_of_range_positive() {
let fitted = fit_inv_fixture();
let probe = Array2::from_shape_vec((1, 2), vec![9.0_f64, 0.0]).unwrap();
assert!(
fitted.inverse_transform(&probe).is_err(),
"index 9 with 2 categories -> sklearn IndexError; ferrolearn must Err"
);
}
#[test]
fn green_inverse_negative_wraps_like_numpy() {
let fitted = fit_inv_fixture();
let probe = Array2::from_shape_vec((1, 2), vec![-1.0_f64, 0.0]).unwrap();
let out = fitted.inverse_transform(&probe).unwrap();
assert_eq!(out[[0, 0]], "dog");
assert_eq!(out[[0, 1]], "x");
let probe2 = Array2::from_shape_vec((1, 2), vec![-2.0_f64, 0.0]).unwrap();
let out2 = fitted.inverse_transform(&probe2).unwrap();
assert_eq!(out2[[0, 0]], "cat");
let probe3 = Array2::from_shape_vec((1, 2), vec![-3.0_f64, 0.0]).unwrap();
assert!(fitted.inverse_transform(&probe3).is_err());
}
#[test]
fn green_inverse_non_integer_truncates_like_numpy() {
let fitted = fit_inv_fixture();
let probe = Array2::from_shape_vec((1, 2), vec![1.5_f64, 0.0]).unwrap();
let out = fitted.inverse_transform(&probe).unwrap();
assert_eq!(out[[0, 0]], "dog"); assert_eq!(out[[0, 1]], "x");
let probe2 = Array2::from_shape_vec((1, 2), vec![0.7_f64, 0.0]).unwrap();
let out2 = fitted.inverse_transform(&probe2).unwrap();
assert_eq!(out2[[0, 0]], "cat"); }
#[test]
fn red_inverse_ncols_mismatch() {
let fitted = fit_inv_fixture();
let probe = Array2::from_shape_vec((1, 3), vec![1.0_f64, 0.0, 2.0]).unwrap();
assert!(
fitted.inverse_transform(&probe).is_err(),
"3 cols when 2 expected -> sklearn ValueError; ferrolearn must Err"
);
}
#[test]
fn red_inverse_zero_row() {
let fitted = fit_inv_fixture();
let probe: Array2<f64> = Array2::from_shape_vec((0, 2), vec![]).unwrap();
assert!(
fitted.inverse_transform(&probe).is_err(),
"0-row inverse -> sklearn ValueError (check_array); ferrolearn must Err"
);
}
#[test]
fn red_inverse_use_encoded_value_unknown_cell() {
let enc = OrdinalEncoder::new()
.with_handle_unknown(HandleUnknown::UseEncodedValue)
.with_unknown_value(-1.0);
let x_train = make_2col(&[("cat", "x"), ("dog", "y"), ("cat", "z")]);
let fitted = enc.fit(&x_train, &()).unwrap();
let probe = Array2::from_shape_vec((1, 2), vec![-1.0_f64, 0.0]).unwrap();
assert!(
fitted.inverse_transform(&probe).is_err(),
"use_encoded_value cell -> sklearn [[None,'x']]; ferrolearn errors (Array2<String> can't hold None)"
);
}
#[test]
fn req10_feature_names_out_and_n_features_in() {
use ferrolearn_core::traits::Fit;
use ferrolearn_preprocess::OrdinalEncoder;
use ndarray::array;
let x = array![
["cat".to_string(), "x".to_string()],
["dog".to_string(), "y".to_string()]
];
let fitted = OrdinalEncoder::new().fit(&x, &()).unwrap();
assert_eq!(fitted.n_features_in(), 2);
let names = fitted.get_feature_names_out(None).unwrap();
assert_eq!(names, vec!["x0".to_string(), "x1".to_string()]);
let custom = vec!["a".to_string(), "b".to_string()];
let named = fitted.get_feature_names_out(Some(&custom)).unwrap();
assert_eq!(named, custom);
let bad = vec!["only_one".to_string()];
assert!(fitted.get_feature_names_out(Some(&bad)).is_err());
}
#[test]
fn green_explicit_given_order_not_sorted() {
let enc = OrdinalEncoder::new().with_categories(cats(&[&["dog", "cat", "bird"]]));
let x = make_1col(&["cat", "dog"]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(
fitted.categories()[0],
["dog", "cat", "bird"],
"categories_ in given order (oracle)"
);
let probe = make_1col(&["cat", "dog", "bird"]);
let out: Array2<f64> = fitted.transform(&probe).unwrap();
assert_eq!(out[[0, 0]], 1.0, "cat -> index 1 (given order)");
assert_eq!(out[[1, 0]], 0.0, "dog -> index 0 (given order)");
assert_eq!(out[[2, 0]], 2.0, "bird -> index 2 (given order)");
}
#[test]
fn green_explicit_unsorted_accepted() {
let enc = OrdinalEncoder::new().with_categories(cats(&[&["zebra", "ant", "moose"]]));
let x = make_1col(&["ant", "zebra"]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(
fitted.categories()[0],
["zebra", "ant", "moose"],
"unsorted explicit accepted, order preserved (oracle)"
);
}
#[test]
fn red_explicit_error_mode_data_not_in_cats_fits_err() {
let enc = OrdinalEncoder::new().with_categories(cats(&[&["cat", "dog"]]));
let x = make_1col(&["cat", "fish"]);
assert!(
enc.fit(&x, &()).is_err(),
"sklearn ValueError 'Found unknown categories ... during fit'; ferrolearn must Err"
);
}
#[test]
fn green_explicit_use_encoded_value_out_of_set_ok() {
let enc = OrdinalEncoder::new()
.with_categories(cats(&[&["cat", "dog"]]))
.with_handle_unknown(HandleUnknown::UseEncodedValue)
.with_unknown_value(-1.0);
let x_train = make_1col(&["cat", "fish"]);
let fitted = enc.fit(&x_train, &()).unwrap();
let probe = make_1col(&["fish", "cat"]);
let out: Array2<f64> = fitted.transform(&probe).unwrap();
assert_eq!(out[[0, 0]], -1.0, "out-of-set 'fish' -> unknown_value -1");
assert_eq!(out[[1, 0]], 0.0, "'cat' -> index 0 (given order)");
}
#[test]
fn red_explicit_n_features_mismatch() {
let enc = OrdinalEncoder::new().with_categories(cats(&[&["cat", "dog"]]));
let x = make_2col(&[("cat", "x"), ("dog", "y")]);
assert!(
enc.fit(&x, &()).is_err(),
"1 cat-list for 2 features -> sklearn ValueError (shape mismatch); ferrolearn must Err"
);
}
#[test]
fn green_explicit_multifeature_each_own_order() {
let enc = OrdinalEncoder::new().with_categories(cats(&[&["dog", "cat"], &["z", "y", "x"]]));
let x = make_2col(&[("cat", "x"), ("dog", "y")]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(fitted.categories()[0], ["dog", "cat"], "col0 given order");
assert_eq!(fitted.categories()[1], ["z", "y", "x"], "col1 given order");
let probe = make_2col(&[("cat", "x"), ("dog", "z")]);
let out: Array2<f64> = fitted.transform(&probe).unwrap();
assert_eq!(out[[0, 0]], 1.0, "col0 cat -> 1");
assert_eq!(out[[0, 1]], 2.0, "col1 x -> 2");
assert_eq!(out[[1, 0]], 0.0, "col0 dog -> 0");
assert_eq!(out[[1, 1]], 0.0, "col1 z -> 0");
}
#[test]
fn red_explicit_duplicate_categories() {
let enc = OrdinalEncoder::new().with_categories(cats(&[&["cat", "cat", "dog"]]));
let x = make_1col(&["cat", "dog"]);
assert!(
enc.fit(&x, &()).is_err(),
"duplicate explicit categories -> sklearn ValueError; ferrolearn must Err"
);
}
#[test]
fn green_explicit_inverse_roundtrip_given_order() {
let enc = OrdinalEncoder::new().with_categories(cats(&[&["dog", "cat", "bird"]]));
let x = make_1col(&["cat", "dog"]);
let fitted = enc.fit(&x, &()).unwrap();
let probe = Array2::from_shape_vec((3, 1), vec![1.0_f64, 0.0, 2.0]).unwrap();
let out = fitted.inverse_transform(&probe).unwrap();
assert_eq!(out[[0, 0]], "cat", "index 1 -> 'cat' (given order)");
assert_eq!(out[[1, 0]], "dog", "index 0 -> 'dog' (given order)");
assert_eq!(out[[2, 0]], "bird", "index 2 -> 'bird' (given order)");
}
#[test]
fn green_explicit_auto_still_default() {
let enc = OrdinalEncoder::new();
let x = make_1col(&["dog", "cat", "bird"]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(
fitted.categories()[0],
["bird", "cat", "dog"],
"auto -> sorted-unique (oracle)"
);
let probe = make_1col(&["cat", "dog", "bird"]);
let out: Array2<f64> = fitted.transform(&probe).unwrap();
assert_eq!(out[[0, 0]], 1.0, "auto cat -> 1");
assert_eq!(out[[1, 0]], 2.0, "auto dog -> 2");
assert_eq!(out[[2, 0]], 0.0, "auto bird -> 0");
}
fn make_1col_rep(spec: &[(&str, usize)]) -> Array2<String> {
let mut vals: Vec<String> = Vec::new();
for &(v, c) in spec {
for _ in 0..c {
vals.push(v.to_string());
}
}
let n = vals.len();
Array2::from_shape_vec((n, 1), vals).unwrap()
}
#[test]
fn req8_min_frequency_two_categories_transform_inverse() {
let enc = OrdinalEncoder::new().with_min_frequency(2);
let x = make_1col_rep(&[("a", 5), ("b", 5), ("c", 1), ("d", 1)]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(
fitted.categories()[0],
["a", "b", "c", "d"],
"categories_ keeps all (oracle)"
);
assert_eq!(
fitted.infrequent_categories()[0],
["c", "d"],
"infrequent_categories_ (oracle)"
);
let probe = make_1col(&["a", "b", "c", "d"]);
let out = fitted.transform(&probe).unwrap();
assert_eq!(out[[0, 0]], 0.0, "a -> 0");
assert_eq!(out[[1, 0]], 1.0, "b -> 1");
assert_eq!(out[[2, 0]], 2.0, "c -> 2 (infrequent)");
assert_eq!(out[[3, 0]], 2.0, "d -> 2 (infrequent, same code)");
let inv = fitted
.inverse_transform(&Array2::from_shape_vec((3, 1), vec![0.0, 1.0, 2.0]).unwrap())
.unwrap();
assert_eq!(inv[[0, 0]], "a", "inverse 0 -> 'a' (frequent)");
assert_eq!(inv[[1, 0]], "b", "inverse 1 -> 'b' (frequent)");
assert_eq!(
inv[[2, 0]],
"infrequent_sklearn",
"inverse 2 -> 'infrequent_sklearn' (shared infrequent code)"
);
}
#[test]
fn req8_max_categories_keeps_top_k_minus_one() {
let enc = OrdinalEncoder::new().with_max_categories(3);
let x = make_1col_rep(&[("a", 5), ("b", 4), ("c", 3), ("d", 2), ("e", 1)]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(fitted.categories()[0], ["a", "b", "c", "d", "e"]);
assert_eq!(
fitted.infrequent_categories()[0],
["c", "d", "e"],
"top-2 frequent (a,b), rest infrequent (oracle)"
);
let probe = make_1col(&["a", "b", "c", "d", "e"]);
let out = fitted.transform(&probe).unwrap();
assert_eq!(
out.column(0).to_vec(),
vec![0.0, 1.0, 2.0, 2.0, 2.0],
"a->0 b->1 c,d,e->2 (oracle)"
);
let inv = fitted
.inverse_transform(&Array2::from_shape_vec((3, 1), vec![0.0, 1.0, 2.0]).unwrap())
.unwrap();
assert_eq!(inv.column(0).to_vec(), vec!["a", "b", "infrequent_sklearn"]);
}
#[test]
fn req8_max_categories_tiebreak_favors_larger_index() {
let enc = OrdinalEncoder::new().with_max_categories(3);
let x = make_1col_rep(&[("a", 5), ("b", 3), ("c", 3), ("d", 1)]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(
fitted.infrequent_categories()[0],
["b", "d"],
"tie favors larger index: b infrequent, c frequent (oracle)"
);
let probe = make_1col(&["a", "b", "c", "d"]);
let out = fitted.transform(&probe).unwrap();
assert_eq!(
out.column(0).to_vec(),
vec![0.0, 2.0, 1.0, 2.0],
"a->0 c->1 (2nd frequent slot) b,d->2 (oracle)"
);
let inv = fitted
.inverse_transform(&Array2::from_shape_vec((3, 1), vec![0.0, 1.0, 2.0]).unwrap())
.unwrap();
assert_eq!(
inv.column(0).to_vec(),
vec!["a", "c", "infrequent_sklearn"],
"inverse 1 -> 'c' (frequent), 2 -> infrequent_sklearn (oracle)"
);
}
#[test]
fn req8_both_set_multifeature_some_without_infrequent() {
let enc = OrdinalEncoder::new()
.with_min_frequency(2)
.with_max_categories(10);
let x = make_2col(&[
("a", "p"),
("a", "p"),
("a", "p"),
("a", "p"),
("a", "p"),
("b", "q"),
("b", "q"),
("b", "q"),
("b", "q"),
("b", "q"),
("c", "p"),
("d", "q"),
]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(fitted.categories()[0], ["a", "b", "c", "d"]);
assert_eq!(fitted.categories()[1], ["p", "q"]);
assert_eq!(fitted.infrequent_categories()[0], ["c", "d"]);
assert!(
fitted.infrequent_categories()[1].is_empty(),
"col 1 has no infrequent categories (sklearn None == empty)"
);
let probe = make_2col(&[("a", "p"), ("b", "q"), ("c", "p"), ("d", "q")]);
let out = fitted.transform(&probe).unwrap();
assert_eq!(out[[0, 0]], 0.0);
assert_eq!(out[[0, 1]], 0.0);
assert_eq!(out[[1, 0]], 1.0);
assert_eq!(out[[1, 1]], 1.0);
assert_eq!(out[[2, 0]], 2.0, "c -> 2 (infrequent col 0)");
assert_eq!(out[[2, 1]], 0.0, "p -> 0 (frequent col 1, unchanged)");
assert_eq!(out[[3, 0]], 2.0, "d -> 2 (infrequent col 0)");
assert_eq!(out[[3, 1]], 1.0, "q -> 1 (frequent col 1, unchanged)");
}
#[test]
fn req8_zero_thresholds_rejected() {
let x = make_1col(&["a", "b"]);
let r_min = OrdinalEncoder::new().with_min_frequency(0).fit(&x, &());
assert!(r_min.is_err(), "min_frequency=0 -> Err (oracle)");
let r_max = OrdinalEncoder::new().with_max_categories(0).fit(&x, &());
assert!(r_max.is_err(), "max_categories=0 -> Err (oracle)");
}
#[test]
fn req8_infrequent_plus_use_encoded_value_distinct_codes() {
let enc = OrdinalEncoder::new()
.with_min_frequency(2)
.with_handle_unknown(HandleUnknown::UseEncodedValue)
.with_unknown_value(-1.0);
let x = make_1col_rep(&[("a", 5), ("b", 5), ("c", 1)]);
let fitted = enc.fit(&x, &()).unwrap();
let probe = make_1col(&["a", "c", "zzz"]);
let out = fitted.transform(&probe).unwrap();
assert_eq!(out[[0, 0]], 0.0, "a -> 0 (frequent)");
assert_eq!(out[[1, 0]], 2.0, "c -> 2 (known infrequent)");
assert_eq!(out[[2, 0]], -1.0, "zzz -> -1 (unknown_value, distinct)");
}
#[test]
fn req8_unknown_value_collision_uses_effective_code_count() {
let x = make_1col_rep(&[("a", 5), ("b", 5), ("c", 1), ("d", 1)]);
let ok = OrdinalEncoder::new()
.with_min_frequency(2)
.with_handle_unknown(HandleUnknown::UseEncodedValue)
.with_unknown_value(3.0)
.fit(&x, &());
assert!(ok.is_ok(), "uv=3 OK under infrequent (3 codes) (oracle)");
let collide = OrdinalEncoder::new()
.with_min_frequency(2)
.with_handle_unknown(HandleUnknown::UseEncodedValue)
.with_unknown_value(2.0)
.fit(&x, &());
assert!(collide.is_err(), "uv=2 collides infrequent code (oracle)");
let base = OrdinalEncoder::new()
.with_handle_unknown(HandleUnknown::UseEncodedValue)
.with_unknown_value(3.0)
.fit(&x, &());
assert!(base.is_err(), "uv=3 collides without grouping (REQ-5)");
}
#[test]
fn req8_disabled_default_unchanged() {
let enc = OrdinalEncoder::new();
let x = make_1col(&["x", "y", "x"]);
let fitted = enc.fit(&x, &()).unwrap();
assert_eq!(fitted.categories()[0], ["x", "y"]);
assert!(
fitted.infrequent_categories()[0].is_empty(),
"disabled -> no infrequent (oracle: no _infrequent_indices)"
);
let out = fitted.transform(&x).unwrap();
assert_eq!(out.column(0).to_vec(), vec![0.0, 1.0, 0.0]);
let inv = fitted.inverse_transform(&out).unwrap();
assert_eq!(inv.column(0).to_vec(), vec!["x", "y", "x"]);
}
#[test]
fn req8_inverse_infrequent_non_roundtrip() {
let enc = OrdinalEncoder::new().with_min_frequency(2);
let x = make_1col_rep(&[("a", 5), ("b", 5), ("c", 1), ("d", 1)]);
let fitted = enc.fit(&x, &()).unwrap();
let probe = make_1col(&["a", "c", "d"]);
let t = fitted.transform(&probe).unwrap();
assert_eq!(t.column(0).to_vec(), vec![0.0, 2.0, 2.0]);
let inv = fitted.inverse_transform(&t).unwrap();
assert_eq!(
inv.column(0).to_vec(),
vec!["a", "infrequent_sklearn", "infrequent_sklearn"],
"frequent 'a' round-trips; infrequent 'c'/'d' -> 'infrequent_sklearn' (oracle)"
);
}
#[test]
fn req8_interleaved_infrequent_remap_and_inverse() {
use ferrolearn_core::traits::Fit;
use ferrolearn_preprocess::OrdinalEncoder;
let mut rows: Vec<&str> = Vec::new();
rows.extend(std::iter::repeat_n("a", 5));
rows.extend(std::iter::repeat_n("b", 1));
rows.extend(std::iter::repeat_n("c", 4));
rows.extend(std::iter::repeat_n("d", 1));
rows.extend(std::iter::repeat_n("e", 3));
let x = make_1col(&rows);
let fitted = OrdinalEncoder::new()
.with_min_frequency(2)
.fit(&x, &())
.unwrap();
assert_eq!(fitted.infrequent_categories()[0], vec!["b", "d"]);
let probe = make_1col(&["a", "b", "c", "d", "e"]);
let out = fitted.transform(&probe).unwrap();
let got: Vec<f64> = out.iter().copied().collect();
assert_eq!(got, vec![0.0, 3.0, 1.0, 3.0, 2.0], "interleaved remap");
let inv_in = ndarray::Array2::from_shape_vec((4, 1), vec![0.0_f64, 1.0, 2.0, 3.0]).unwrap();
let inv = fitted.inverse_transform(&inv_in).unwrap();
assert_eq!(inv[[0, 0]], "a");
assert_eq!(inv[[1, 0]], "c");
assert_eq!(inv[[2, 0]], "e");
assert_eq!(inv[[3, 0]], "infrequent_sklearn");
}