pub(crate) use super::*;
#[test]
fn test_new() {
let model = BayesianLogisticRegression::new(1.0);
assert!(model.coefficients_map.is_none());
assert!(model.posterior_covariance.is_none());
}
#[test]
fn test_builder_pattern() {
let model = BayesianLogisticRegression::new(1.0)
.with_learning_rate(0.1)
.with_max_iter(500)
.with_tolerance(1e-3);
assert!(model.coefficients_map.is_none());
}
#[test]
fn test_fit_simple() {
let x = Matrix::from_vec(6, 1, vec![-2.0, -1.0, -0.5, 0.5, 1.0, 2.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
let mut model = BayesianLogisticRegression::new(0.1);
let result = model.fit(&x, &y);
assert!(result.is_ok(), "Fit should succeed");
assert!(model.coefficients_map.is_some());
assert!(model.posterior_covariance.is_some());
let beta = model
.coefficients_map
.as_ref()
.expect("MAP estimate exists");
assert!(
beta[0] > 0.0,
"Coefficient should be positive, got {}",
beta[0]
);
}
#[test]
fn test_predict_proba() {
let x = Matrix::from_vec(4, 1, vec![-1.0, -0.5, 0.5, 1.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.0, 1.0, 1.0]);
let mut model = BayesianLogisticRegression::new(0.1);
model.fit(&x, &y).expect("Fit succeeds");
let x_test = Matrix::from_vec(3, 1, vec![-2.0, 0.0, 2.0]).expect("Valid test matrix");
let probas = model.predict_proba(&x_test).expect("Prediction succeeds");
assert_eq!(probas.len(), 3);
for &p in probas.as_slice() {
assert!(
(0.0..=1.0).contains(&p),
"Probability should be in [0,1], got {p}"
);
}
assert!(probas[0] < probas[1], "P(y=1 | x=-2) < P(y=1 | x=0)");
assert!(probas[1] < probas[2], "P(y=1 | x=0) < P(y=1 | x=2)");
}
#[test]
fn test_predict() {
let x = Matrix::from_vec(4, 1, vec![-1.0, -0.5, 0.5, 1.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.0, 1.0, 1.0]);
let mut model = BayesianLogisticRegression::new(0.1);
model.fit(&x, &y).expect("Fit succeeds");
let x_test = Matrix::from_vec(2, 1, vec![-2.0, 2.0]).expect("Valid test matrix");
let labels = model.predict(&x_test).expect("Prediction succeeds");
assert_eq!(labels.len(), 2);
for &label in labels.as_slice() {
assert!(
label == 0.0 || label == 1.0,
"Label should be 0 or 1, got {label}"
);
}
}
#[test]
fn test_fit_dimension_mismatch() {
let x =
Matrix::from_vec(4, 2, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 1.0]);
let mut model = BayesianLogisticRegression::new(1.0);
let result = model.fit(&x, &y);
assert!(result.is_err());
let err = result.expect_err("Should be an error");
assert!(matches!(err, AprenderError::DimensionMismatch { .. }));
}
#[test]
fn test_fit_invalid_labels() {
let x = Matrix::from_vec(3, 1, vec![1.0, 2.0, 3.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.5, 1.0]);
let mut model = BayesianLogisticRegression::new(1.0);
let result = model.fit(&x, &y);
assert!(result.is_err());
let err = result.expect_err("Should be an error");
assert!(matches!(err, AprenderError::Other(_)));
}
#[test]
fn test_predict_not_fitted() {
let model = BayesianLogisticRegression::new(1.0);
let x_test = Matrix::from_vec(2, 1, vec![1.0, 2.0]).expect("Valid matrix");
let result = model.predict_proba(&x_test);
assert!(result.is_err());
let err = result.expect_err("Should be an error");
assert!(matches!(err, AprenderError::Other(_)));
}
#[test]
fn test_predict_dimension_mismatch() {
let x =
Matrix::from_vec(4, 2, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.0, 1.0, 1.0]);
let mut model = BayesianLogisticRegression::new(1.0);
model.fit(&x, &y).expect("Fit succeeds");
let x_test = Matrix::from_vec(2, 1, vec![1.0, 2.0]).expect("Valid test matrix");
let result = model.predict_proba(&x_test);
assert!(result.is_err());
let err = result.expect_err("Should be an error");
assert!(matches!(err, AprenderError::DimensionMismatch { .. }));
}
#[test]
fn test_map_convergence() {
let x = Matrix::from_vec(6, 1, vec![-2.0, -1.0, -0.5, 0.5, 1.0, 2.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
let mut model = BayesianLogisticRegression::new(0.1)
.with_max_iter(2000)
.with_tolerance(1e-5);
let result = model.fit(&x, &y);
assert!(result.is_ok(), "MAP estimation should converge");
}
#[test]
fn test_map_non_convergence() {
let x = Matrix::from_vec(6, 1, vec![-2.0, -1.0, -0.5, 0.5, 1.0, 2.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
let mut model = BayesianLogisticRegression::new(0.1)
.with_max_iter(5) .with_tolerance(1e-10);
let result = model.fit(&x, &y);
assert!(result.is_err(), "Should fail to converge");
let err = result.expect_err("Should be an error");
assert!(matches!(err, AprenderError::Other(_)));
}
#[test]
fn test_predict_proba_interval() {
let x = Matrix::from_vec(6, 1, vec![-2.0, -1.0, -0.5, 0.5, 1.0, 2.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
let mut model = BayesianLogisticRegression::new(0.1);
model.fit(&x, &y).expect("Fit succeeds");
let x_test = Matrix::from_vec(3, 1, vec![-2.0, 0.0, 2.0]).expect("Valid test matrix");
let (lower, upper) = model
.predict_proba_interval(&x_test, 0.95)
.expect("Interval prediction succeeds");
assert_eq!(lower.len(), 3);
assert_eq!(upper.len(), 3);
let probas = model.predict_proba(&x_test).expect("Prediction succeeds");
for i in 0..3 {
assert!(
lower[i] <= probas[i],
"Lower bound should be <= point estimate: {i}: {} <= {}",
lower[i],
probas[i]
);
assert!(
probas[i] <= upper[i],
"Upper bound should be >= point estimate: {i}: {} >= {}",
probas[i],
upper[i]
);
assert!(
lower[i] >= 0.0 && lower[i] <= 1.0,
"Lower bound should be in [0,1], got {}",
lower[i]
);
assert!(
upper[i] >= 0.0 && upper[i] <= 1.0,
"Upper bound should be in [0,1], got {}",
upper[i]
);
}
for i in 0..3 {
assert!(
upper[i] >= lower[i],
"Upper bound should be >= lower bound at {i}: {} >= {}",
upper[i],
lower[i]
);
}
let max_width = (0..3).map(|i| upper[i] - lower[i]).fold(0.0_f32, f32::max);
assert!(
max_width > 0.01,
"At least one interval should have width > 0.01, max was {max_width}"
);
}
#[test]
fn test_predict_interval_not_fitted() {
let model = BayesianLogisticRegression::new(1.0);
let x_test = Matrix::from_vec(2, 1, vec![1.0, 2.0]).expect("Valid matrix");
let result = model.predict_proba_interval(&x_test, 0.95);
assert!(result.is_err());
let err = result.expect_err("Should be an error");
assert!(matches!(err, AprenderError::Other(_)));
}
#[test]
fn test_sigmoid_extreme_values() {
let sig_neg = BayesianLogisticRegression::sigmoid(-100.0);
assert!(sig_neg < 1e-10);
let sig_pos = BayesianLogisticRegression::sigmoid(100.0);
assert!(sig_pos > 0.9999999);
let sig_zero = BayesianLogisticRegression::sigmoid(0.0);
assert!((sig_zero - 0.5).abs() < 1e-6);
}
#[test]
fn test_prior_precision_effects() {
let x = Matrix::from_vec(6, 1, vec![-2.0, -1.0, -0.5, 0.5, 1.0, 2.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
let mut model_low = BayesianLogisticRegression::new(0.1);
model_low.fit(&x, &y).expect("Fit succeeds");
let mut model_high = BayesianLogisticRegression::new(10.0);
model_high.fit(&x, &y).expect("Fit succeeds");
let beta_low = model_low
.coefficients_map
.as_ref()
.expect("has coefficients");
let beta_high = model_high
.coefficients_map
.as_ref()
.expect("has coefficients");
assert!(
beta_low[0].abs() >= beta_high[0].abs(),
"Higher prior precision should shrink coefficients"
);
}
#[test]
fn test_multiple_features() {
let x = Matrix::from_vec(4, 2, vec![1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, -1.0])
.expect("Valid matrix");
let y = Vector::from_vec(vec![1.0, 1.0, 0.0, 0.0]);
let mut model = BayesianLogisticRegression::new(0.1);
model.fit(&x, &y).expect("Fit succeeds");
let beta = model.coefficients_map.as_ref().unwrap();
assert!(beta.len() >= 2);
}
#[test]
fn test_predict_interval_dimension_mismatch() {
let x =
Matrix::from_vec(4, 2, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.0, 1.0, 1.0]);
let mut model = BayesianLogisticRegression::new(1.0);
model.fit(&x, &y).expect("Fit succeeds");
let x_test = Matrix::from_vec(2, 1, vec![1.0, 2.0]).expect("Valid test matrix");
let result = model.predict_proba_interval(&x_test, 0.95);
assert!(result.is_err());
}
#[test]
fn test_predict_returns_labels() {
let x = Matrix::from_vec(6, 1, vec![-2.0, -1.0, -0.5, 0.5, 1.0, 2.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
let mut model = BayesianLogisticRegression::new(0.1);
model.fit(&x, &y).expect("Fit succeeds");
let x_test = Matrix::from_vec(4, 1, vec![-3.0, -0.1, 0.1, 3.0]).expect("Valid test matrix");
let labels = model.predict(&x_test).expect("Prediction succeeds");
for &label in labels.as_slice() {
assert!(label == 0.0 || label == 1.0);
}
assert_eq!(labels[0], 0.0);
assert_eq!(labels[3], 1.0);
}
#[test]
fn test_wide_credible_interval() {
let x = Matrix::from_vec(6, 1, vec![-2.0, -1.0, -0.5, 0.5, 1.0, 2.0]).expect("Valid matrix");
let y = Vector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
let mut model = BayesianLogisticRegression::new(0.1);
model.fit(&x, &y).expect("Fit succeeds");
let x_test = Matrix::from_vec(1, 1, vec![0.0]).expect("Valid test matrix");
let (lower_90, upper_90) = model
.predict_proba_interval(&x_test, 0.90)
.expect("Interval succeeds");
let (lower_99, upper_99) = model
.predict_proba_interval(&x_test, 0.99)
.expect("Interval succeeds");
let width_90 = upper_90[0] - lower_90[0];
let width_99 = upper_99[0] - lower_99[0];
assert!(
width_99 >= width_90,
"99% CI should be wider than 90% CI: {} >= {}",
width_99,
width_90
);
}
fn reference_map_1d(x_col: &[f32], y: &[f32], lambda: f32) -> f32 {
let mut beta = 0.0_f64;
let lambda = f64::from(lambda);
for _ in 0..200 {
let mut g = -lambda * beta;
let mut h = lambda;
for (&xi, &yi) in x_col.iter().zip(y.iter()) {
let xi = f64::from(xi);
let z = xi * beta;
let p = 1.0 / (1.0 + (-z).exp());
g += xi * (f64::from(yi) - p);
h += xi * xi * p * (1.0 - p);
}
let step = g / h;
beta += step;
if step.abs() < 1e-12 {
break;
}
}
beta as f32
}
#[test]
fn test_pmat864_map_targets_declared_precision_not_n_lambda() {
let x_col = vec![-3.0_f32, -2.0, -1.0, -0.5, 0.5, 1.0, 2.0, 3.0];
let y_vec = vec![0.0_f32, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
let n = x_col.len();
let lambda = 0.2_f32;
let x = Matrix::from_vec(n, 1, x_col.clone()).expect("Valid matrix");
let y = Vector::from_vec(y_vec.clone());
let mut model = BayesianLogisticRegression::new(lambda)
.with_learning_rate(0.3)
.with_max_iter(200_000)
.with_tolerance(1e-5);
model.fit(&x, &y).expect("Fit should converge");
let beta_fitted = model
.coefficients_map
.as_ref()
.expect("MAP estimate exists")[0];
let beta_lambda = reference_map_1d(&x_col, &y_vec, lambda);
let beta_n_lambda = reference_map_1d(&x_col, &y_vec, lambda * n as f32);
assert!(
beta_lambda.abs() > 2.0 * beta_n_lambda.abs(),
"test design: λ-mode ({beta_lambda}) should be far larger in magnitude \
than the n·λ-mode ({beta_n_lambda})"
);
assert!(
(beta_fitted - beta_lambda).abs() < 1e-2,
"PMAT-864: fitted β ({beta_fitted}) must converge to the λ-MAP \
({beta_lambda}), not the over-shrunk n·λ-MAP ({beta_n_lambda}). \
A match to the n·λ-MAP indicates the data term is divided by n while \
the prior term is not (gradient/Hessian inconsistency)."
);
assert!(
(beta_fitted - beta_n_lambda).abs() > 0.5 * (beta_lambda - beta_n_lambda).abs(),
"PMAT-864: fitted β ({beta_fitted}) is too close to the over-shrunk \
n·λ-MAP ({beta_n_lambda}) — the 1/n data-term scaling has crept back in."
);
}
#[test]
fn test_pmat864_replicating_samples_does_not_shrink_map() {
let base_x = [-3.0_f32, -2.0, -1.0, -0.5, 0.5, 1.0, 2.0, 3.0];
let base_y = [0.0_f32, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
let lambda = 0.2_f32;
let fit_beta = |reps: usize| -> f32 {
let mut xv = Vec::new();
let mut yv = Vec::new();
for _ in 0..reps {
xv.extend_from_slice(&base_x);
yv.extend_from_slice(&base_y);
}
let n = xv.len();
let x = Matrix::from_vec(n, 1, xv).expect("Valid matrix");
let y = Vector::from_vec(yv);
let mut model = BayesianLogisticRegression::new(lambda)
.with_learning_rate(0.3)
.with_max_iter(200_000)
.with_tolerance(1e-5);
model.fit(&x, &y).expect("Fit should converge");
model
.coefficients_map
.as_ref()
.expect("MAP estimate exists")[0]
};
let beta_1x = fit_beta(1);
let beta_2x = fit_beta(2);
assert!(
beta_2x.abs() >= beta_1x.abs() - 1e-3,
"PMAT-864: doubling the sample count (same distribution) shrank the MAP \
from {beta_1x} to {beta_2x} — the data term is being averaged by n \
(precision-n·λ signature)."
);
assert!(
beta_2x.abs() > beta_1x.abs(),
"PMAT-864: doubling evidence at fixed λ must strictly grow |β| \
({beta_1x} -> {beta_2x})."
);
}