use crate::algorithms::count_to_f64;
use crate::error::{Error, Result};
use crate::likelihood::{LogLikelihood, MleFit};
impl crate::likelihood::ExponentialLikelihood {
pub fn fit(&self, data: &[f64]) -> Result<MleFit> {
if data.is_empty() {
return Err(Error::InsufficientData);
}
let mut sum = 0.0_f64;
for &x in data {
if !x.is_finite() || x < 0.0 {
return Err(Error::InvalidInput(format!(
"exponential data must be finite and >= 0, got {x}"
)));
}
sum += x;
}
if sum <= 0.0 {
return Err(Error::DegenerateInput(
"all observations are zero, so the rate MLE n / sum(x) is infinite".to_owned(),
));
}
let n = count_to_f64(data.len());
let lambda_hat = n / sum;
let log_likelihood = self.log_likelihood(&[lambda_hat], data);
Ok(MleFit::from_closed_form(
vec![lambda_hat],
log_likelihood,
data.len(),
))
}
}
impl LogLikelihood for crate::likelihood::ExponentialLikelihood {
fn n_params(&self) -> usize {
1
}
fn log_likelihood(&self, params: &[f64], data: &[f64]) -> f64 {
let lambda = *params.first().unwrap_or(&0.0);
if lambda <= 0.0 || lambda.is_nan() {
return f64::NEG_INFINITY;
}
let mut sum = 0.0_f64;
for &x in data {
if !x.is_finite() || x < 0.0 {
return f64::NEG_INFINITY;
}
sum += x;
}
let n = count_to_f64(data.len());
n.mul_add(lambda.ln(), -lambda * sum)
}
}
#[cfg(test)]
mod tests {
use crate::error::Error;
use crate::likelihood::ExponentialLikelihood;
use crate::likelihood::{LogLikelihood, fit_mle};
const DATA: [f64; 5] = [0.5, 1.2, 2.3, 0.8, 3.1];
const LAMBDA_HAT: f64 = 0.632_911_392_405_063_2;
const LL_AT_HALF: f64 = -7.415_735_902_799_727;
const LL_AT_HAT: f64 = -7.287_124_235_194_378;
const AIC_AT_HAT: f64 = 16.574_248_470_388_756;
const BIC_AT_HAT: f64 = 16.183_686_382_822_856;
fn model() -> ExponentialLikelihood {
ExponentialLikelihood::default()
}
fn rel_close(a: f64, b: f64, rel: f64) -> bool {
(a - b).abs() <= rel * b.abs().max(1.0)
}
fn is_neg_inf(x: f64) -> bool {
x.is_infinite() && x.is_sign_negative()
}
fn first_param(fit: &crate::likelihood::MleFit) -> f64 {
fit.params().first().copied().unwrap_or(f64::NAN)
}
#[test]
fn fit_rejects_empty_data() {
let got = model().fit(&[]);
assert!(
matches!(got, Err(Error::InsufficientData)),
"empty fit was {got:?}"
);
}
#[test]
fn fit_rejects_negative_observation() {
let got = model().fit(&[1.0, -0.5, 2.0]);
assert!(
matches!(got, Err(Error::InvalidInput(_))),
"negative fit was {got:?}"
);
}
#[test]
fn fit_rejects_all_zero_data() {
let got = model().fit(&[0.0, 0.0, 0.0]);
assert!(
matches!(got, Err(Error::DegenerateInput(_))),
"all-zero fit was {got:?}"
);
}
#[test]
fn log_likelihood_matches_scipy() {
let got = model().log_likelihood(&[0.5], &DATA);
assert!(
rel_close(got, LL_AT_HALF, 1e-10),
"ll@0.5 was {got}, expected {LL_AT_HALF}"
);
}
#[test]
fn log_likelihood_is_neg_inf_for_nonpositive_rate() {
let m = model();
assert!(
is_neg_inf(m.log_likelihood(&[0.0], &DATA)),
"ll at rate 0 was {}",
m.log_likelihood(&[0.0], &DATA)
);
assert!(
is_neg_inf(m.log_likelihood(&[-1.0], &DATA)),
"ll at rate -1 was {}",
m.log_likelihood(&[-1.0], &DATA)
);
}
#[test]
fn log_likelihood_is_neg_inf_for_negative_observation() {
let got = model().log_likelihood(&[1.0], &[1.0, -2.0]);
assert!(is_neg_inf(got), "ll with negative x was {got}");
}
#[test]
fn log_likelihood_non_finite_observation_is_neg_inf() {
let m = model();
assert!(
is_neg_inf(m.log_likelihood(&[1.0], &[1.0, f64::NAN, 2.0])),
"NaN observation should give NEG_INFINITY, got {}",
m.log_likelihood(&[1.0], &[1.0, f64::NAN, 2.0])
);
assert!(
is_neg_inf(m.log_likelihood(&[1.0], &[1.0, f64::INFINITY, 2.0])),
"+inf observation should give NEG_INFINITY, got {}",
m.log_likelihood(&[1.0], &[1.0, f64::INFINITY, 2.0])
);
}
#[test]
fn fit_recovers_closed_form_estimate() -> Result<(), Error> {
let fit = model().fit(&DATA)?;
let lambda = first_param(&fit);
assert!(
(lambda - LAMBDA_HAT).abs() <= 1e-12,
"lambda_hat was {lambda}"
);
assert!(
rel_close(fit.log_likelihood(), LL_AT_HAT, 1e-10),
"ll was {}",
fit.log_likelihood()
);
assert!(fit.converged(), "closed-form fit should report converged");
assert_eq!(fit.iterations(), 0, "closed-form fit does zero iterations");
assert!(
rel_close(fit.aic(), AIC_AT_HAT, 1e-10),
"aic was {}",
fit.aic()
);
assert!(
rel_close(fit.bic(), BIC_AT_HAT, 1e-10),
"bic was {}",
fit.bic()
);
Ok(())
}
#[test]
fn fit_mle_from_perturbed_init_recovers_lambda_hat() -> Result<(), Error> {
let fit = fit_mle(&model(), &DATA, &[0.9], 1e-10)?;
let lambda = first_param(&fit);
assert!(
(lambda - LAMBDA_HAT).abs() <= 1e-5,
"lambda_hat from optimizer was {lambda}"
);
Ok(())
}
}