use ferrolearn_core::{Fit, Predict, Transform};
use ferrolearn_test_oracle::{json_to_array1, json_to_array2, load_fixture};
#[test]
fn conformance_extra_tree_classifier() {
let fx = load_fixture("extra_tree_classifier");
let x = json_to_array2(&fx.input["X"]);
let y_vec: Vec<usize> = fx.input["y"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let y = ndarray::Array1::from_vec(y_vec);
let max_depth = fx.params["max_depth"].as_u64().map(|v| v as usize);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_tree::ExtraTreeClassifier::<f64>::new()
.with_max_depth(max_depth)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("ExtraTreeClassifier fit");
let preds = fitted.predict(&x).expect("ExtraTreeClassifier predict");
let expected_acc = fx.expected["accuracy"].as_f64().unwrap_or(0.5);
let acc = preds.iter().zip(y.iter()).filter(|(a, e)| a == e).count() as f64 / y.len() as f64;
assert!(
acc >= 0.85 * expected_acc,
"ExtraTreeClassifier accuracy {acc:.4} < 0.85 * sklearn {expected_acc:.4}"
);
}
#[test]
fn conformance_extra_tree_regressor() {
let fx = load_fixture("extra_tree_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let max_depth = fx.params["max_depth"].as_u64().map(|v| v as usize);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_tree::ExtraTreeRegressor::<f64>::new()
.with_max_depth(max_depth)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("ExtraTreeRegressor fit");
let preds = fitted.predict(&x).expect("ExtraTreeRegressor predict");
let y_mean = y.iter().sum::<f64>() / y.len() as f64;
let ss_tot: f64 = y.iter().map(|v| (v - y_mean).powi(2)).sum();
let ss_res: f64 = preds.iter().zip(y.iter()).map(|(a, e)| (a - e).powi(2)).sum();
let r2 = 1.0 - ss_res / ss_tot;
let expected_r2 = fx.expected["r2"].as_f64().unwrap_or(0.5);
assert!(
r2 >= expected_r2 - 0.1,
"ExtraTreeRegressor R² {r2:.4} < sklearn-0.1 ({expected_r2:.4})"
);
}
#[test]
fn conformance_extra_trees_classifier() {
let fx = load_fixture("extra_trees_classifier");
let x = json_to_array2(&fx.input["X"]);
let y_vec: Vec<usize> = fx.input["y"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let y = ndarray::Array1::from_vec(y_vec);
let n_est = fx.params["n_estimators"].as_u64().unwrap_or(20) as usize;
let max_depth = fx.params["max_depth"].as_u64().map(|v| v as usize);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_tree::ExtraTreesClassifier::<f64>::new()
.with_n_estimators(n_est)
.with_max_depth(max_depth)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("ExtraTrees fit");
let preds = fitted.predict(&x).expect("ExtraTrees predict");
let expected_acc = fx.expected["accuracy"].as_f64().unwrap_or(0.5);
let acc = preds.iter().zip(y.iter()).filter(|(a, e)| a == e).count() as f64 / y.len() as f64;
assert!(
acc >= 0.95 * expected_acc,
"ExtraTreesClassifier accuracy {acc:.4} < 0.95 * sklearn {expected_acc:.4}"
);
}
#[test]
fn conformance_extra_trees_regressor() {
let fx = load_fixture("extra_trees_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let n_est = fx.params["n_estimators"].as_u64().unwrap_or(20) as usize;
let max_depth = fx.params["max_depth"].as_u64().map(|v| v as usize);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_tree::ExtraTreesRegressor::<f64>::new()
.with_n_estimators(n_est)
.with_max_depth(max_depth)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("ExtraTreesRegressor fit");
let preds = fitted.predict(&x).expect("ExtraTreesRegressor predict");
let y_mean = y.iter().sum::<f64>() / y.len() as f64;
let ss_tot: f64 = y.iter().map(|v| (v - y_mean).powi(2)).sum();
let ss_res: f64 = preds.iter().zip(y.iter()).map(|(a, e)| (a - e).powi(2)).sum();
let r2 = 1.0 - ss_res / ss_tot;
let expected_r2 = fx.expected["r2"].as_f64().unwrap_or(0.5);
assert!(
r2 >= expected_r2 - 0.1,
"ExtraTreesRegressor R² {r2:.4} < sklearn - 0.1 ({expected_r2:.4})"
);
}
#[test]
fn conformance_bagging_classifier() {
let fx = load_fixture("bagging_classifier");
let x = json_to_array2(&fx.input["X"]);
let y_vec: Vec<usize> = fx.input["y"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let y = ndarray::Array1::from_vec(y_vec);
let n_est = fx.params["n_estimators"].as_u64().unwrap_or(10) as usize;
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_tree::BaggingClassifier::<f64>::new()
.with_n_estimators(n_est)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("Bagging fit");
let preds = fitted.predict(&x).expect("Bagging predict");
let expected_acc = fx.expected["accuracy"].as_f64().unwrap_or(0.5);
let acc = preds.iter().zip(y.iter()).filter(|(a, e)| a == e).count() as f64 / y.len() as f64;
assert!(
acc >= 0.85 * expected_acc,
"BaggingClassifier accuracy {acc:.4} < 0.85 * sklearn {expected_acc:.4}"
);
}
#[test]
fn conformance_bagging_regressor() {
let fx = load_fixture("bagging_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let n_est = fx.params["n_estimators"].as_u64().unwrap_or(10) as usize;
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_tree::BaggingRegressor::<f64>::new()
.with_n_estimators(n_est)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("Bagging fit");
let preds = fitted.predict(&x).expect("Bagging predict");
let y_mean = y.iter().sum::<f64>() / y.len() as f64;
let ss_tot: f64 = y.iter().map(|v| (v - y_mean).powi(2)).sum();
let ss_res: f64 = preds.iter().zip(y.iter()).map(|(a, e)| (a - e).powi(2)).sum();
let r2 = 1.0 - ss_res / ss_tot;
let expected_r2 = fx.expected["r2"].as_f64().unwrap_or(0.5);
assert!(
r2 >= expected_r2 - 0.15,
"BaggingRegressor R² {r2:.4} < sklearn - 0.15 ({expected_r2:.4})"
);
}
#[test]
fn conformance_adaboost_regressor() {
let fx = load_fixture("adaboost_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let n_est = fx.params["n_estimators"].as_u64().unwrap_or(20) as usize;
let lr = fx.params["learning_rate"].as_f64().unwrap_or(1.0);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_tree::AdaBoostRegressor::<f64>::new()
.with_n_estimators(n_est)
.with_learning_rate(lr)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("AdaBoostRegressor fit");
let preds = fitted.predict(&x).expect("AdaBoostRegressor predict");
let y_mean = y.iter().sum::<f64>() / y.len() as f64;
let ss_tot: f64 = y.iter().map(|v| (v - y_mean).powi(2)).sum();
let ss_res: f64 = preds.iter().zip(y.iter()).map(|(a, e)| (a - e).powi(2)).sum();
let r2 = 1.0 - ss_res / ss_tot;
let expected_r2 = fx.expected["r2"].as_f64().unwrap_or(0.5);
assert!(
r2 >= expected_r2 - 0.2,
"AdaBoostRegressor R² {r2:.4} < sklearn - 0.2 ({expected_r2:.4})"
);
}
#[test]
fn conformance_hist_gradient_boosting_classifier() {
let fx = load_fixture("hist_gradient_boosting_classifier");
let x = json_to_array2(&fx.input["X"]);
let y_vec: Vec<usize> = fx.input["y"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let y = ndarray::Array1::from_vec(y_vec);
let n_est = fx.params["max_iter"].as_u64().unwrap_or(50) as usize;
let max_depth = fx.params["max_depth"].as_u64().map(|v| v as usize);
let lr = fx.params["learning_rate"].as_f64().unwrap_or(0.1);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_tree::HistGradientBoostingClassifier::<f64>::new()
.with_n_estimators(n_est)
.with_max_depth(max_depth)
.with_learning_rate(lr)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("HistGBC fit");
let preds = fitted.predict(&x).expect("HistGBC predict");
let expected_acc = fx.expected["accuracy"].as_f64().unwrap_or(0.5);
let acc = preds.iter().zip(y.iter()).filter(|(a, e)| a == e).count() as f64 / y.len() as f64;
assert!(
acc >= 0.90 * expected_acc,
"HistGBC accuracy {acc:.4} < 0.90 * sklearn {expected_acc:.4}"
);
}
#[test]
fn conformance_hist_gradient_boosting_regressor() {
let fx = load_fixture("hist_gradient_boosting_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let n_est = fx.params["max_iter"].as_u64().unwrap_or(50) as usize;
let max_depth = fx.params["max_depth"].as_u64().map(|v| v as usize);
let lr = fx.params["learning_rate"].as_f64().unwrap_or(0.1);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_tree::HistGradientBoostingRegressor::<f64>::new()
.with_n_estimators(n_est)
.with_max_depth(max_depth)
.with_learning_rate(lr)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("HistGBR fit");
let preds = fitted.predict(&x).expect("HistGBR predict");
let y_mean = y.iter().sum::<f64>() / y.len() as f64;
let ss_tot: f64 = y.iter().map(|v| (v - y_mean).powi(2)).sum();
let ss_res: f64 = preds.iter().zip(y.iter()).map(|(a, e)| (a - e).powi(2)).sum();
let r2 = 1.0 - ss_res / ss_tot;
let expected_r2 = fx.expected["r2"].as_f64().unwrap_or(0.5);
assert!(
r2 >= expected_r2 - 0.1,
"HistGBR R² {r2:.4} < sklearn - 0.1 ({expected_r2:.4})"
);
}
#[test]
fn conformance_isolation_forest() {
let fx = load_fixture("isolation_forest");
let x = json_to_array2(&fx.input["X"]);
let n_est = fx.params["n_estimators"].as_u64().unwrap_or(50) as usize;
let contamination = fx.params["contamination"].as_f64().unwrap_or(0.1);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_tree::IsolationForest::<f64>::new()
.with_n_estimators(n_est)
.with_contamination(contamination)
.with_random_state(random_state);
let fitted = model.fit(&x, &()).expect("IsolationForest fit");
let preds = fitted.predict(&x).expect("IsolationForest predict");
let expected: Vec<i64> = fx.expected["predictions"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_i64().unwrap())
.collect();
let matches = preds
.iter()
.zip(expected.iter())
.filter(|&(&a, &e)| a as i64 == e)
.count();
let frac = matches as f64 / preds.len() as f64;
assert!(
frac >= 0.80,
"IsolationForest +1/-1 agreement {frac:.4} < 0.80 floor"
);
}
#[test]
fn conformance_random_trees_embedding() {
let fx = load_fixture("random_trees_embedding");
let x = json_to_array2(&fx.input["X"]);
let n_est = fx.params["n_estimators"].as_u64().unwrap_or(20) as usize;
let max_depth = fx.params["max_depth"].as_u64().map(|v| v as usize);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_tree::RandomTreesEmbedding::<f64>::new()
.with_n_estimators(n_est)
.with_max_depth(max_depth)
.with_random_state(random_state);
let fitted = model.fit(&x, &()).expect("RandomTreesEmbedding fit");
let xt = fitted.transform(&x).expect("RandomTreesEmbedding transform");
assert_eq!(xt.nrows(), x.nrows(), "embedding rows");
assert!(xt.ncols() > 0, "embedding cols must be > 0");
}
#[test]
fn conformance_voting_classifier() {
let fx = load_fixture("voting_classifier");
let x = json_to_array2(&fx.input["X"]);
let y_vec: Vec<usize> = fx.input["y"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let y = ndarray::Array1::from_vec(y_vec);
let model = ferrolearn_tree::VotingClassifier::<f64>::new()
.with_max_depths(vec![Some(5), Some(5)]);
let fitted = model.fit(&x, &y).expect("VotingClassifier fit");
let preds = fitted.predict(&x).expect("VotingClassifier predict");
let expected_acc = fx.expected["accuracy"].as_f64().unwrap_or(0.5);
let acc = preds.iter().zip(y.iter()).filter(|(a, e)| a == e).count() as f64 / y.len() as f64;
assert!(
acc >= 0.80 * expected_acc,
"VotingClassifier accuracy {acc:.4} < 0.80 * sklearn {expected_acc:.4} \
(note: ferrolearn voting uses DT-only ensemble; not directly comparable to sklearn LR+DT)"
);
}
#[test]
fn conformance_voting_regressor() {
let fx = load_fixture("voting_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let model = ferrolearn_tree::VotingRegressor::<f64>::new()
.with_max_depths(vec![Some(5), Some(5)]);
let fitted = model.fit(&x, &y).expect("VotingRegressor fit");
let preds = fitted.predict(&x).expect("VotingRegressor predict");
let y_mean = y.iter().sum::<f64>() / y.len() as f64;
let ss_tot: f64 = y.iter().map(|v| (v - y_mean).powi(2)).sum();
let ss_res: f64 = preds.iter().zip(y.iter()).map(|(a, e)| (a - e).powi(2)).sum();
let r2 = 1.0 - ss_res / ss_tot;
assert!(r2 >= 0.0, "VotingRegressor R² {r2:.4} below baseline (mean prediction)");
}