use statrs::distribution::{ChiSquared, ContinuousCDF};
use super::LogisticFit;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HosmerLemeshow {
pub statistic: f64,
pub df: usize,
pub p_value: f64,
pub groups: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GoodnessOfFit {
pub null_deviance: f64,
pub residual_deviance: f64,
pub df_null: f64,
pub df_residual: f64,
pub mcfadden_r2: f64,
pub aic: f64,
pub bic: f64,
}
impl LogisticFit {
pub fn goodness_of_fit(&self) -> GoodnessOfFit {
let n = self.n_observations() as f64;
let p = self.n_parameters() as f64;
let y = self.response();
let residual_deviance = -2.0 * self.log_likelihood();
let ybar = y.sum() / n;
let ll_null: f64 = y
.iter()
.map(|&yi| yi * ybar.ln() + (1.0 - yi) * (1.0 - ybar).ln())
.sum();
let null_deviance = -2.0 * ll_null;
let mcfadden_r2 = if ll_null != 0.0 {
1.0 - self.log_likelihood() / ll_null
} else {
f64::NAN
};
GoodnessOfFit {
null_deviance,
residual_deviance,
df_null: n - 1.0,
df_residual: n - p,
mcfadden_r2,
aic: residual_deviance + 2.0 * p,
bic: residual_deviance + n.ln() * p,
}
}
pub fn hosmer_lemeshow(&self, groups: usize) -> HosmerLemeshow {
let n = self.n_observations();
let y = self.response();
let p = self.fitted_probabilities();
if groups < 3 || n < groups + 1 {
return HosmerLemeshow {
statistic: f64::NAN,
df: groups.saturating_sub(2),
p_value: f64::NAN,
groups,
};
}
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| p[a].partial_cmp(&p[b]).unwrap_or(std::cmp::Ordering::Equal));
let mut statistic = 0.0;
let mut used_groups = 0usize;
for g in 0..groups {
let start = g * n / groups;
let end = (g + 1) * n / groups;
if end <= start {
continue;
}
let ng = (end - start) as f64;
let mut observed = 0.0;
let mut expected = 0.0;
for &idx in &order[start..end] {
observed += y[idx];
expected += p[idx];
}
let pbar = expected / ng;
if pbar <= 0.0 || pbar >= 1.0 {
continue;
}
let diff = observed - expected;
statistic += diff * diff / (ng * pbar * (1.0 - pbar));
used_groups += 1;
}
let df = used_groups.saturating_sub(2);
let p_value = match ChiSquared::new(df as f64) {
Ok(dist) if df >= 1 && statistic.is_finite() => 1.0 - dist.cdf(statistic),
_ => f64::NAN,
};
HosmerLemeshow {
statistic,
df,
p_value,
groups: used_groups,
}
}
}