use ferrolearn_core::error::FerroError;
use ferrolearn_core::traits::{Fit, Predict};
use ferrolearn_linear::{ClassifierScore, QDA, RegressorScore};
use ndarray::{Array1, Array2, array};
struct FixedRegressor {
preds: Array1<f64>,
}
impl Predict<Array2<f64>> for FixedRegressor {
type Output = Array1<f64>;
type Error = FerroError;
fn predict(&self, _x: &Array2<f64>) -> Result<Self::Output, Self::Error> {
Ok(self.preds.clone())
}
}
struct FixedClassifier {
preds: Array1<usize>,
}
impl Predict<Array2<f64>> for FixedClassifier {
type Output = Array1<usize>;
type Error = FerroError;
fn predict(&self, _x: &Array2<f64>) -> Result<Self::Output, Self::Error> {
Ok(self.preds.clone())
}
}
#[test]
fn divergence_r2_constant_ytrue_nonzero_residual_returns_zero() {
const SK_R2_CONST_Y_RESID: f64 = 0.0;
let reg = FixedRegressor {
preds: Array1::from(vec![4.0_f64, 5.0, 6.0]),
};
let x = Array2::<f64>::zeros((3, 1)); let y = Array1::from(vec![5.0_f64, 5.0, 5.0]);
let score = reg.score(&x, &y, None).expect("score should succeed");
assert_eq!(
score, SK_R2_CONST_Y_RESID,
"constant y_true with non-zero residual: sklearn r2_score returns {SK_R2_CONST_Y_RESID}, \
ferrolearn returned {score} (tracking #1104)",
);
}
#[test]
fn r2_constant_ytrue_zero_residual_returns_one() {
const SK_R2_CONST_Y_PERFECT: f64 = 1.0;
let reg = FixedRegressor {
preds: Array1::from(vec![5.0_f64, 5.0, 5.0]),
};
let x = Array2::<f64>::zeros((3, 1));
let y = Array1::from(vec![5.0_f64, 5.0, 5.0]);
let score = reg.score(&x, &y, None).expect("score should succeed");
assert_eq!(score, SK_R2_CONST_Y_PERFECT);
}
#[test]
fn r2_in_regime_matches_oracle() {
const SK_R2_IN_REGIME: f64 = 0.9152542372881356;
let reg = FixedRegressor {
preds: Array1::from(vec![2.5_f64, 5.0, 2.0, 8.0]),
};
let x = Array2::<f64>::zeros((4, 1));
let y = Array1::from(vec![3.0_f64, 5.0, 2.0, 7.0]);
let score = reg.score(&x, &y, None).expect("score should succeed");
assert!(
(score - SK_R2_IN_REGIME).abs() < 1e-8,
"in-regime R^2: sklearn {SK_R2_IN_REGIME}, ferrolearn {score}",
);
}
#[test]
fn divergence_log_proba_zero_clamps_instead_of_neg_inf() {
let x: Array2<f64> = array![[0.0], [1.0], [100.0], [200.0]];
let y = Array1::from(vec![0usize, 0, 1, 1]);
let fitted = QDA::new().fit(&x, &y).expect("QDA fit should succeed");
let xq: Array2<f64> = array![[-1.0e6]];
let proba = fitted.predict_proba(&xq).expect("predict_proba");
assert_eq!(
proba[[0, 0]],
0.0,
"precondition: ferrolearn QDA proba[0,0] must underflow to exact 0.0 \
(sklearn oracle proba == [[0.0, 1.0]]); got {}",
proba[[0, 0]],
);
let logp = fitted
.predict_log_proba(&xq)
.expect("predict_log_proba should succeed");
let sk_log_proba_of_zero = f64::NEG_INFINITY;
assert_eq!(
logp[[0, 0]],
sk_log_proba_of_zero,
"log of exact-0 probability: sklearn np.log gives -inf \
(discriminant_analysis.py:1059), ferrolearn clamp gives {} (tracking #1105)",
logp[[0, 0]],
);
}
#[test]
fn classifier_score_weighted_matches_sklearn() {
const SK_WEIGHTED_ACC: f64 = 0.625;
let clf = FixedClassifier {
preds: Array1::from(vec![0usize, 1, 0, 0, 1]),
};
let x = Array2::<f64>::zeros((5, 1)); let y = Array1::from(vec![0usize, 1, 1, 0, 1]);
let w = Array1::from(vec![1.0_f64, 2.0, 3.0, 1.0, 1.0]);
let score = clf
.score(&x, &y, Some(&w))
.expect("weighted score should succeed");
assert!(
(score - SK_WEIGHTED_ACC).abs() < 1e-12,
"weighted accuracy: sklearn {SK_WEIGHTED_ACC}, ferrolearn {score}",
);
}
#[test]
fn regressor_score_weighted_matches_sklearn() {
const SK_WEIGHTED_R2: f64 = 0.9770114942528736;
let reg = FixedRegressor {
preds: Array1::from(vec![1.1_f64, 1.9, 3.2, 3.7, 5.1]),
};
let x = Array2::<f64>::zeros((5, 1));
let y = Array1::from(vec![1.0_f64, 2.0, 3.0, 4.0, 5.0]);
let w = Array1::from(vec![1.0_f64, 2.0, 3.0, 1.0, 1.0]);
let score = reg
.score(&x, &y, Some(&w))
.expect("weighted score should succeed");
assert!(
(score - SK_WEIGHTED_R2).abs() < 1e-12,
"weighted R^2: sklearn {SK_WEIGHTED_R2}, ferrolearn {score}",
);
}
#[test]
fn score_none_sample_weight_equals_unweighted() {
let reg = FixedRegressor {
preds: Array1::from(vec![2.5_f64, 5.0, 2.0, 8.0]),
};
let x = Array2::<f64>::zeros((4, 1));
let y = Array1::from(vec![3.0_f64, 5.0, 2.0, 7.0]);
let none_score = reg.score(&x, &y, None).expect("score None should succeed");
const SK_R2_IN_REGIME: f64 = 0.9152542372881356;
assert_eq!(
none_score, SK_R2_IN_REGIME,
"score(x, y, None) must be byte-identical to the unweighted R^2",
);
let clf = FixedClassifier {
preds: Array1::from(vec![0usize, 1, 0, 0, 1]),
};
let yc = Array1::from(vec![0usize, 1, 1, 0, 1]);
let acc_none = clf
.score(&Array2::<f64>::zeros((5, 1)), &yc, None)
.expect("classifier None score");
assert_eq!(acc_none, 0.8_f64);
}