use ferrolearn_core::introspection::HasCoefficients;
use ferrolearn_core::{Fit, Predict};
use ferrolearn_test_oracle::{
TOL_ITERATIVE_LINEAR_ABS, TOL_ITERATIVE_LINEAR_REL, TOL_LINEAR_FIT_ABS, TOL_LINEAR_FIT_REL,
assert_close, assert_close_slice, json_to_array1, json_to_array2, load_fixture,
};
fn gamma_scale(x: &ndarray::Array2<f64>) -> f64 {
let n_features = x.ncols() as f64;
let flat: Vec<f64> = x.iter().copied().collect();
let n = flat.len() as f64;
let mean = flat.iter().sum::<f64>() / n;
let var = flat.iter().map(|v| (v - mean) * (v - mean)).sum::<f64>() / n;
1.0 / (n_features * var)
}
#[test]
fn conformance_huber_regressor() {
let fx = load_fixture("huber_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(5e-3, 5e-3);
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(200) as usize;
let alpha = fx.params["alpha"].as_f64().unwrap_or(1e-4);
let epsilon = fx.params["epsilon"].as_f64().unwrap_or(1.35);
let model = ferrolearn_linear::HuberRegressor::<f64>::new()
.with_max_iter(max_iter)
.with_alpha(alpha)
.with_epsilon(epsilon);
let fitted = model.fit(&x, &y).expect("Huber fit");
let expected_coefs = json_to_array1(&fx.expected["coefficients"]);
assert_close_slice(
fitted.coefficients().as_slice().unwrap(),
expected_coefs.as_slice().unwrap(),
rel,
abs,
"Huber.coefficients",
);
let expected_intercept = fx.expected["intercept"].as_f64().unwrap();
assert_close(
fitted.intercept(),
expected_intercept,
rel,
abs,
"Huber.intercept",
);
}
#[test]
fn conformance_bayesian_ridge() {
let fx = load_fixture("bayesian_ridge");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(TOL_ITERATIVE_LINEAR_REL, TOL_ITERATIVE_LINEAR_ABS);
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(300) as usize;
let tol = fx.params["tol"].as_f64().unwrap_or(1e-3);
let model = ferrolearn_linear::BayesianRidge::<f64>::new()
.with_max_iter(max_iter)
.with_tol(tol);
let fitted = model.fit(&x, &y).expect("BayesianRidge fit");
let expected_coefs = json_to_array1(&fx.expected["coefficients"]);
assert_close_slice(
fitted.coefficients().as_slice().unwrap(),
expected_coefs.as_slice().unwrap(),
rel,
abs,
"BayesianRidge.coefficients",
);
let expected_intercept = fx.expected["intercept"].as_f64().unwrap();
assert_close(
fitted.intercept(),
expected_intercept,
rel,
abs,
"BayesianRidge.intercept",
);
}
#[test]
fn conformance_ard_regression() {
let fx = load_fixture("ard_regression");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(1e-3, 1e-5);
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(300) as usize;
let tol = fx.params["tol"].as_f64().unwrap_or(1e-3);
let model = ferrolearn_linear::ARDRegression::<f64>::new()
.with_max_iter(max_iter)
.with_tol(tol);
let fitted = model.fit(&x, &y).expect("ARD fit");
let expected_coefs = json_to_array1(&fx.expected["coefficients"]);
assert_close_slice(
fitted.coefficients().as_slice().unwrap(),
expected_coefs.as_slice().unwrap(),
rel,
abs,
"ARD.coefficients",
);
let expected_intercept = fx.expected["intercept"].as_f64().unwrap();
assert_close(
fitted.intercept(),
expected_intercept,
rel,
abs,
"ARD.intercept",
);
}
#[test]
fn conformance_quantile_regressor() {
let fx = load_fixture("quantile_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(5e-3, 5e-2);
let quantile = fx.params["quantile"].as_f64().unwrap_or(0.5);
let alpha = fx.params["alpha"].as_f64().unwrap_or(0.01);
let model = ferrolearn_linear::QuantileRegressor::<f64>::new()
.with_quantile(quantile)
.with_alpha(alpha);
let fitted = model.fit(&x, &y).expect("Quantile fit");
let expected_preds = json_to_array1(&fx.expected["predictions"]);
let preds = fitted.predict(&x).expect("Quantile predict");
assert_close_slice(
preds.as_slice().unwrap(),
expected_preds.as_slice().unwrap(),
rel,
abs,
"Quantile.predict",
);
}
#[test]
fn conformance_lars() {
let fx = load_fixture("lars");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(TOL_LINEAR_FIT_REL, TOL_LINEAR_FIT_ABS);
let n_nonzero = fx.params["n_nonzero_coefs"].as_u64().unwrap_or(5) as usize;
let model = ferrolearn_linear::Lars::<f64>::new().with_n_nonzero_coefs(n_nonzero);
let fitted = model.fit(&x, &y).expect("Lars fit");
let expected_coefs = json_to_array1(&fx.expected["coefficients"]);
assert_close_slice(
fitted.coefficients().as_slice().unwrap(),
expected_coefs.as_slice().unwrap(),
rel,
abs,
"Lars.coefficients",
);
let expected_intercept = fx.expected["intercept"].as_f64().unwrap();
assert_close(
fitted.intercept(),
expected_intercept,
rel,
abs,
"Lars.intercept",
);
}
#[test]
fn conformance_lasso_lars() {
let fx = load_fixture("lasso_lars");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(2e-2, 1e-2);
let alpha = fx.params["alpha"].as_f64().unwrap_or(0.1);
let model = ferrolearn_linear::LassoLars::<f64>::new().with_alpha(alpha);
let fitted = model.fit(&x, &y).expect("LassoLars fit");
let expected_coefs = json_to_array1(&fx.expected["coefficients"]);
assert_close_slice(
fitted.coefficients().as_slice().unwrap(),
expected_coefs.as_slice().unwrap(),
rel,
abs,
"LassoLars.coefficients",
);
}
#[test]
fn conformance_orthogonal_matching_pursuit() {
let fx = load_fixture("orthogonal_matching_pursuit");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(TOL_LINEAR_FIT_REL, TOL_LINEAR_FIT_ABS);
let n_nonzero = fx.params["n_nonzero_coefs"].as_u64().unwrap_or(4) as usize;
let model =
ferrolearn_linear::OrthogonalMatchingPursuit::<f64>::new().with_n_nonzero_coefs(n_nonzero);
let fitted = model.fit(&x, &y).expect("OMP fit");
let expected_coefs = json_to_array1(&fx.expected["coefficients"]);
assert_close_slice(
fitted.coefficients().as_slice().unwrap(),
expected_coefs.as_slice().unwrap(),
rel,
abs,
"OMP.coefficients",
);
let expected_intercept = fx.expected["intercept"].as_f64().unwrap();
assert_close(
fitted.intercept(),
expected_intercept,
rel,
abs,
"OMP.intercept",
);
}
#[test]
fn conformance_ridge_cv() {
let fx = load_fixture("ridge_cv");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(TOL_LINEAR_FIT_REL, TOL_LINEAR_FIT_ABS);
let alphas: Vec<f64> = fx.params["alphas"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_f64().unwrap())
.collect();
let cv = fx.params["cv"].as_u64().unwrap_or(5) as usize;
let model = ferrolearn_linear::RidgeCV::<f64>::new()
.with_alphas(alphas)
.with_cv(cv);
let fitted = model.fit(&x, &y).expect("RidgeCV fit");
let expected_alpha = fx.expected["alpha"].as_f64().unwrap();
assert_close(
fitted.best_alpha(),
expected_alpha,
rel,
abs,
"RidgeCV.best_alpha",
);
}
#[test]
fn conformance_lasso_cv() {
let fx = load_fixture("lasso_cv");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(1e-3, 1e-5);
let cv = fx.params["cv"].as_u64().unwrap_or(5) as usize;
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(2000) as usize;
let model = ferrolearn_linear::LassoCV::<f64>::new()
.with_cv(cv)
.with_max_iter(max_iter);
let fitted = model.fit(&x, &y).expect("LassoCV fit");
let expected_preds = json_to_array1(&fx.expected["predictions"]);
let preds = fitted.predict(&x).expect("LassoCV predict");
assert_close_slice(
preds.as_slice().unwrap(),
expected_preds.as_slice().unwrap(),
rel,
abs,
"LassoCV.predict",
);
}
#[test]
fn conformance_elastic_net_cv() {
let fx = load_fixture("elastic_net_cv");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(1e-3, 1e-5);
let cv = fx.params["cv"].as_u64().unwrap_or(5) as usize;
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(2000) as usize;
let model = ferrolearn_linear::ElasticNetCV::<f64>::new()
.with_l1_ratios(vec![0.5])
.with_cv(cv)
.with_max_iter(max_iter);
let fitted = model.fit(&x, &y).expect("ElasticNetCV fit");
let expected_preds = json_to_array1(&fx.expected["predictions"]);
let preds = fitted.predict(&x).expect("ElasticNetCV predict");
assert_close_slice(
preds.as_slice().unwrap(),
expected_preds.as_slice().unwrap(),
rel,
abs,
"ElasticNetCV.predict",
);
}
#[test]
fn conformance_logistic_regression_cv() {
let fx = load_fixture("logistic_regression_cv");
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 (rel, abs) = fx.tolerance(3e-1, 1e-3);
let cv = fx.params["cv"].as_u64().unwrap_or(5) as usize;
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(2000) as usize;
let _ = (rel, abs); let model = ferrolearn_linear::LogisticRegressionCV::<f64>::new()
.with_cv(cv)
.with_max_iter(max_iter);
let fitted = model.fit(&x, &y).expect("LogisticRegressionCV fit");
let preds = fitted.predict(&x).expect("predict");
let expected_classes: Vec<usize> = fx.expected["predicted_classes"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let matches = preds
.iter()
.zip(expected_classes.iter())
.filter(|(a, e)| a == e)
.count();
let acc = matches as f64 / preds.len() as f64;
assert!(
acc >= 0.85,
"LogisticRegressionCV accuracy {acc:.4} below 0.85 floor"
);
}
#[test]
fn conformance_lda() {
let fx = load_fixture("lda");
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 (rel, abs) = fx.tolerance(2e-1, 5e-2);
let model = ferrolearn_linear::LDA::<f64>::new(None);
let fitted = model.fit(&x, &y).expect("LDA fit");
let preds: Vec<usize> = fitted.predict(&x).expect("LDA predict").to_vec();
let expected_classes: Vec<usize> = fx.expected["predicted_classes"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let matches: usize = preds
.iter()
.zip(expected_classes.iter())
.filter(|(a, e)| a == e)
.count();
let acc = matches as f64 / preds.len() as f64;
assert!(acc >= 0.90, "LDA.predict accuracy floor 0.90, got {acc:.4}");
let _proba = fitted.predict_proba(&x).expect("LDA predict_proba");
let _expected_proba = json_to_array2(&fx.expected["predicted_proba"]);
let _ = (rel, abs); }
#[test]
fn conformance_qda() {
let fx = load_fixture("qda");
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 (rel, abs) = fx.tolerance(1e-4, 1e-6);
let model = ferrolearn_linear::QDA::<f64>::new();
let fitted = model.fit(&x, &y).expect("QDA fit");
let preds: Vec<usize> = fitted.predict(&x).expect("QDA predict").to_vec();
let expected_classes: Vec<usize> = fx.expected["predicted_classes"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let matches: usize = preds
.iter()
.zip(expected_classes.iter())
.filter(|(a, e)| a == e)
.count();
let acc = matches as f64 / preds.len() as f64;
assert!(acc >= 0.95, "QDA.predict accuracy floor 0.95, got {acc:.4}");
let proba = fitted.predict_proba(&x).expect("QDA predict_proba");
let expected_proba = json_to_array2(&fx.expected["predicted_proba"]);
assert_close_slice(
proba.as_slice().unwrap(),
expected_proba.as_slice().unwrap(),
rel,
abs,
"QDA.predict_proba",
);
}
#[test]
fn conformance_ridge_classifier() {
let fx = load_fixture("ridge_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 alpha = fx.params["alpha"].as_f64().unwrap_or(1.0);
let model = ferrolearn_linear::RidgeClassifier::<f64>::new().with_alpha(alpha);
let fitted = model.fit(&x, &y).expect("RidgeClassifier fit");
let preds: Vec<usize> = fitted
.predict(&x)
.expect("RidgeClassifier predict")
.to_vec();
let expected_classes: Vec<usize> = fx.expected["predicted_classes"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let matches: usize = preds
.iter()
.zip(expected_classes.iter())
.filter(|(a, e)| a == e)
.count();
let acc = matches as f64 / preds.len() as f64;
assert!(
acc >= 0.95,
"RidgeClassifier.predict accuracy {acc:.4} < 0.95 floor"
);
}
#[test]
fn conformance_linear_svc() {
let fx = load_fixture("linear_svc");
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 c = fx.params["C"].as_f64().unwrap_or(1.0);
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(2000) as usize;
let model = ferrolearn_linear::LinearSVC::<f64>::new()
.with_c(c)
.with_max_iter(max_iter);
let fitted = model.fit(&x, &y).expect("LinearSVC fit");
let preds: Vec<usize> = fitted.predict(&x).expect("LinearSVC predict").to_vec();
let expected_classes: Vec<usize> = fx.expected["predicted_classes"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let matches: usize = preds
.iter()
.zip(expected_classes.iter())
.filter(|(a, e)| a == e)
.count();
let acc = matches as f64 / preds.len() as f64;
assert!(
acc >= 0.90,
"LinearSVC.predict accuracy {acc:.4} < 0.90 floor"
);
}
#[test]
fn conformance_linear_svr() {
let fx = load_fixture("linear_svr");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let c = fx.params["C"].as_f64().unwrap_or(1.0);
let epsilon = fx.params["epsilon"].as_f64().unwrap_or(0.1);
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(2000) as usize;
let model = ferrolearn_linear::LinearSVR::<f64>::new()
.with_c(c)
.with_epsilon(epsilon)
.with_max_iter(max_iter);
let fitted = model.fit(&x, &y).expect("LinearSVR fit");
let preds = fitted.predict(&x).expect("LinearSVR predict");
let y_slice = y.as_slice().unwrap();
let y_mean = y_slice.iter().sum::<f64>() / y_slice.len() as f64;
let ss_tot: f64 = y_slice.iter().map(|v| (v - y_mean).powi(2)).sum();
let ss_res: f64 = preds
.iter()
.zip(y_slice.iter())
.map(|(&a, &e)| (a - e).powi(2))
.sum();
let r2 = 1.0 - ss_res / ss_tot;
assert!(
r2 >= 0.40,
"LinearSVR R² {r2:.4} below 0.40 floor (sklearn reaches ~0.97 on the same data)"
);
}
#[test]
fn conformance_ransac_regressor() {
let fx = load_fixture("ransac_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let min_samples = fx.params["min_samples"].as_f64().unwrap_or(0.5);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let base = ferrolearn_linear::LinearRegression::<f64>::new();
let model = ferrolearn_linear::RANSACRegressor::<f64, _>::new(base)
.with_min_samples((min_samples * x.nrows() as f64) as usize)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("RANSAC fit");
let expected_mask: Vec<bool> = fx.expected["inlier_mask"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() != 0)
.collect();
let actual_mask = fitted.inlier_mask();
assert_eq!(
actual_mask.len(),
expected_mask.len(),
"RANSAC.inlier_mask length"
);
let agreement: usize = actual_mask
.iter()
.zip(expected_mask.iter())
.filter(|(a, e)| a == e)
.count();
let frac = agreement as f64 / actual_mask.len() as f64;
assert!(
frac >= 0.70,
"RANSAC.inlier_mask agreement {frac:.4} below 0.70 floor (random-sample-consensus seed variance)"
);
}
#[test]
fn conformance_isotonic_regression() {
let fx = load_fixture("isotonic_regression");
let x_vec = json_to_array1(&fx.input["x"]);
let n = x_vec.len();
let x = ndarray::Array2::from_shape_vec((n, 1), x_vec.as_slice().unwrap().to_vec()).unwrap();
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(1e-4, 1e-6);
let increasing = fx.params["increasing"].as_bool().unwrap_or(true);
let model = ferrolearn_linear::IsotonicRegression::<f64>::new().with_increasing(increasing);
let fitted = model.fit(&x, &y).expect("Isotonic fit");
let preds = fitted.predict(&x).expect("Isotonic predict");
let expected_preds = json_to_array1(&fx.expected["predictions"]);
assert_close_slice(
preds.as_slice().unwrap(),
expected_preds.as_slice().unwrap(),
rel,
abs,
"Isotonic.predict",
);
}
#[test]
fn conformance_poisson_regressor() {
let fx = load_fixture("poisson_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(2e-1, 1e-1);
let alpha = fx.params["alpha"].as_f64().unwrap_or(0.1);
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(200) as usize;
let model = ferrolearn_linear::PoissonRegressor::<f64>::new()
.with_alpha(alpha)
.with_max_iter(max_iter);
let fitted = model.fit(&x, &y).expect("Poisson fit");
let preds = fitted.predict(&x).expect("Poisson predict");
let expected_preds = json_to_array1(&fx.expected["predictions"]);
assert_close_slice(
preds.as_slice().unwrap(),
expected_preds.as_slice().unwrap(),
rel,
abs,
"Poisson.predict",
);
}
#[test]
fn conformance_gamma_regressor() {
let fx = load_fixture("gamma_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(2e-1, 1e-1);
let alpha = fx.params["alpha"].as_f64().unwrap_or(0.1);
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(200) as usize;
let model = ferrolearn_linear::GammaRegressor::<f64>::new()
.with_alpha(alpha)
.with_max_iter(max_iter);
let fitted = model.fit(&x, &y).expect("Gamma fit");
let preds = fitted.predict(&x).expect("Gamma predict");
let expected_preds = json_to_array1(&fx.expected["predictions"]);
assert_close_slice(
preds.as_slice().unwrap(),
expected_preds.as_slice().unwrap(),
rel,
abs,
"Gamma.predict",
);
}
#[test]
fn conformance_tweedie_regressor() {
let fx = load_fixture("tweedie_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let (rel, abs) = fx.tolerance(2e-1, 1e-1);
let alpha = fx.params["alpha"].as_f64().unwrap_or(0.1);
let power = fx.params["power"].as_f64().unwrap_or(1.5);
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(200) as usize;
let model = ferrolearn_linear::TweedieRegressor::<f64>::new()
.with_power(power)
.with_alpha(alpha)
.with_max_iter(max_iter);
let fitted = model.fit(&x, &y).expect("Tweedie fit");
let preds = fitted.predict(&x).expect("Tweedie predict");
let expected_preds = json_to_array1(&fx.expected["predictions"]);
assert_close_slice(
preds.as_slice().unwrap(),
expected_preds.as_slice().unwrap(),
rel,
abs,
"Tweedie.predict",
);
}
#[test]
fn conformance_sgd_classifier() {
let fx = load_fixture("sgd_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.clone());
let alpha = fx.params["alpha"].as_f64().unwrap_or(1e-4);
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(1000) as usize;
let tol = fx.params["tol"].as_f64().unwrap_or(1e-3);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_linear::SGDClassifier::<f64>::new()
.with_loss(ferrolearn_linear::sgd::ClassifierLoss::Log)
.with_alpha(alpha)
.with_max_iter(max_iter)
.with_tol(tol)
.with_shuffle(false)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("SGDClassifier fit");
let preds: Vec<usize> = fitted.predict(&x).expect("SGDClassifier predict").to_vec();
let acc = preds
.iter()
.zip(y_vec.iter())
.filter(|(a, e)| a == e)
.count() as f64
/ preds.len() as f64;
assert!(
acc >= 0.70,
"SGDClassifier(log_loss, shuffle=false).predict train accuracy {acc:.4} < 0.70 \
floor (live sklearn log_loss/shuffle=False achieves 0.745 on this fixture)"
);
}
#[test]
fn conformance_sgd_regressor() {
let fx = load_fixture("sgd_regressor");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let alpha = fx.params["alpha"].as_f64().unwrap_or(1e-4);
let max_iter = fx.params["max_iter"].as_u64().unwrap_or(1000) as usize;
let tol = fx.params["tol"].as_f64().unwrap_or(1e-3);
let random_state = fx.params["random_state"].as_u64().unwrap_or(42);
let model = ferrolearn_linear::SGDRegressor::<f64>::new()
.with_alpha(alpha)
.with_max_iter(max_iter)
.with_tol(tol)
.with_random_state(random_state);
let fitted = model.fit(&x, &y).expect("SGDRegressor fit");
let preds = fitted.predict(&x).expect("SGDRegressor predict");
let expected_preds = json_to_array1(&fx.expected["predictions"]);
let exp_mean = expected_preds.iter().sum::<f64>() / expected_preds.len() as f64;
let ss_tot: f64 = expected_preds.iter().map(|e| (e - exp_mean).powi(2)).sum();
let ss_res: f64 = preds
.iter()
.zip(expected_preds.iter())
.map(|(&a, &e)| (a - e).powi(2))
.sum();
let r2 = 1.0 - ss_res / ss_tot;
assert!(
r2 >= 0.85,
"SGDRegressor R² with sklearn = {r2:.4}, floor 0.85"
);
}
#[test]
fn conformance_svc_rbf() {
let fx = load_fixture("svc");
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 c = fx.params["C"].as_f64().unwrap_or(1.0);
let tol = 1e-4;
let max_iter = 2000;
let kernel = ferrolearn_linear::RbfKernel::<f64>::with_gamma(gamma_scale(&x));
let model = ferrolearn_linear::SVC::<f64, _>::new(kernel)
.with_c(c)
.with_tol(tol)
.with_max_iter(max_iter);
let fitted = model.fit(&x, &y).expect("SVC fit");
let preds: Vec<usize> = fitted.predict(&x).expect("SVC predict").to_vec();
let expected_classes: Vec<usize> = fx.expected["predicted_classes"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let matches: usize = preds
.iter()
.zip(expected_classes.iter())
.filter(|(a, e)| a == e)
.count();
let acc = matches as f64 / preds.len() as f64;
assert!(
acc >= 0.90,
"SVC(RBF).predict accuracy {acc:.4} < 0.90 floor"
);
}
#[test]
fn conformance_svr_rbf() {
let fx = load_fixture("svr");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let c = fx.params["C"].as_f64().unwrap_or(1.0);
let kernel = ferrolearn_linear::RbfKernel::<f64>::with_gamma(gamma_scale(&x));
let model = ferrolearn_linear::SVR::<f64, _>::new(kernel)
.with_c(c)
.with_tol(1e-4)
.with_max_iter(2000);
let fitted = model.fit(&x, &y).expect("SVR fit");
let preds = fitted.predict(&x).expect("SVR predict");
let expected_preds = json_to_array1(&fx.expected["predictions"]);
let exp_mean = expected_preds.iter().sum::<f64>() / expected_preds.len() as f64;
let ss_tot: f64 = expected_preds.iter().map(|e| (e - exp_mean).powi(2)).sum();
let ss_res: f64 = preds
.iter()
.zip(expected_preds.iter())
.map(|(&a, &e)| (a - e).powi(2))
.sum();
let r2 = 1.0 - ss_res / ss_tot;
assert!(r2 >= 0.80, "SVR(RBF) R² with sklearn = {r2:.4}, floor 0.80");
}
#[test]
fn conformance_nu_svc_rbf() {
let fx = load_fixture("nu_svc");
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 nu = fx.params["nu"].as_f64().unwrap_or(0.5);
let kernel = ferrolearn_linear::RbfKernel::<f64>::with_gamma(gamma_scale(&x));
let model = ferrolearn_linear::NuSVC::<f64, _>::new(kernel)
.with_nu(nu)
.with_tol(1e-4)
.with_max_iter(2000);
let fitted = model.fit(&x, &y).expect("NuSVC fit");
let preds: Vec<usize> = fitted.predict(&x).expect("NuSVC predict").to_vec();
let expected_classes: Vec<usize> = fx.expected["predicted_classes"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let matches: usize = preds
.iter()
.zip(expected_classes.iter())
.filter(|(a, e)| a == e)
.count();
let acc = matches as f64 / preds.len() as f64;
assert!(
acc >= 0.85,
"NuSVC(RBF).predict accuracy {acc:.4} < 0.85 floor"
);
}
#[test]
fn conformance_nu_svr_rbf() {
let fx = load_fixture("nu_svr");
let x = json_to_array2(&fx.input["X"]);
let y = json_to_array1(&fx.input["y"]);
let nu = fx.params["nu"].as_f64().unwrap_or(0.5);
let _c_unused = fx.params["C"].as_f64().unwrap_or(1.0); let kernel = ferrolearn_linear::RbfKernel::<f64>::with_gamma(gamma_scale(&x));
let model = ferrolearn_linear::NuSVR::<f64, _>::new(kernel)
.with_nu(nu)
.with_tol(1e-4)
.with_max_iter(2000);
let fitted = model.fit(&x, &y).expect("NuSVR fit");
let preds = fitted.predict(&x).expect("NuSVR predict");
let expected_preds = json_to_array1(&fx.expected["predictions"]);
let exp_mean = expected_preds.iter().sum::<f64>() / expected_preds.len() as f64;
let ss_tot: f64 = expected_preds.iter().map(|e| (e - exp_mean).powi(2)).sum();
let ss_res: f64 = preds
.iter()
.zip(expected_preds.iter())
.map(|(&a, &e)| (a - e).powi(2))
.sum();
let r2 = 1.0 - ss_res / ss_tot;
assert!(
r2 >= 0.40,
"NuSVR(RBF) R² with sklearn = {r2:.4}, floor 0.40"
);
}
#[test]
fn conformance_one_class_svm_rbf() {
let fx = load_fixture("one_class_svm");
let x = json_to_array2(&fx.input["X"]);
let nu = fx.params["nu"].as_f64().unwrap_or(0.1);
let kernel = ferrolearn_linear::RbfKernel::<f64>::with_gamma(gamma_scale(&x));
let model = ferrolearn_linear::OneClassSVM::<f64, _>::new(kernel)
.with_nu(nu)
.with_tol(1e-4)
.with_max_iter(2000);
let fitted = model.fit(&x, &()).expect("OneClassSVM fit");
let preds = fitted.predict(&x).expect("OneClassSVM predict");
let expected_preds: Vec<isize> = fx.expected["predictions"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_i64().unwrap() as isize)
.collect();
let matches: usize = preds
.iter()
.zip(expected_preds.iter())
.filter(|(a, e)| a == e)
.count();
let frac = matches as f64 / preds.len() as f64;
assert!(
frac >= 0.85,
"OneClassSVM(RBF) +1/-1 label agreement {frac:.4} < 0.85 floor"
);
}