regression_diagnostics/glm/goodness.rs
1use super::family::Family;
2use super::GlmFit;
3
4/// Overall GLM goodness-of-fit statistics.
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub struct GoodnessOfFit {
7 /// Deviance of the intercept-only (mean-only) model.
8 pub null_deviance: f64,
9 /// Residual deviance of the fitted model (the sum of unit deviances, and of
10 /// squared deviance residuals).
11 pub residual_deviance: f64,
12 /// Degrees of freedom of the null deviance (`n − 1`).
13 pub df_null: f64,
14 /// Degrees of freedom of the residual deviance (`n − p`).
15 pub df_residual: f64,
16 /// Estimated dispersion `φ` (`1` for Poisson / negative binomial).
17 pub dispersion: f64,
18 /// McFadden's pseudo-R², `1 − ℓ/ℓ₀`.
19 pub mcfadden_r2: f64,
20 /// Akaike information criterion, `−2ℓ + 2k`.
21 pub aic: f64,
22 /// Bayesian information criterion, `−2ℓ + ln(n)·k`.
23 pub bic: f64,
24}
25
26impl<F: Family> GlmFit<F> {
27 /// Overall goodness-of-fit summary: null/residual deviance, dispersion,
28 /// McFadden's pseudo-R², and AIC/BIC.
29 ///
30 /// The **residual deviance** is the sum of per-observation unit deviances
31 /// (equivalently the sum of squared deviance residuals). The **null model**
32 /// is the mean-only fit, whose MLE mean is the sample mean `ȳ` for all
33 /// log-link families here, so the null deviance is computed directly without
34 /// a second IRLS solve.
35 ///
36 /// The information criteria count `k = p` parameters when the dispersion is
37 /// fixed (Poisson, negative binomial) and `k = p + 1` when it is estimated
38 /// (Gamma), charging the extra degree of freedom for `φ̂`. McFadden's
39 /// pseudo-R² compares the fitted log-likelihood to the mean-only model at the
40 /// same dispersion.
41 pub fn goodness_of_fit(&self) -> GoodnessOfFit {
42 let n = self.n_observations();
43 let p = self.n_parameters();
44 let y = self.response();
45 let mu = self.fitted_means();
46 let family = self.family();
47 let dispersion = self.dispersion();
48
49 let residual_deviance: f64 = (0..n).map(|i| family.unit_deviance(y[i], mu[i])).sum();
50
51 // Null (mean-only) model: μ ≡ ȳ for every log-link family here.
52 let ybar = y.sum() / n as f64;
53 let null_deviance: f64 = (0..n).map(|i| family.unit_deviance(y[i], ybar)).sum();
54
55 let ll = self.log_likelihood();
56 let ll_null: f64 = (0..n).map(|i| family.loglik(y[i], ybar, dispersion)).sum();
57 let mcfadden_r2 = if ll_null != 0.0 {
58 1.0 - ll / ll_null
59 } else {
60 f64::NAN
61 };
62
63 // Estimated dispersion counts as one extra parameter in the criteria.
64 let k = p as f64 + if family.dispersion_known() { 0.0 } else { 1.0 };
65 let aic = -2.0 * ll + 2.0 * k;
66 let bic = -2.0 * ll + (n as f64).ln() * k;
67
68 GoodnessOfFit {
69 null_deviance,
70 residual_deviance,
71 df_null: (n - 1) as f64,
72 df_residual: (n - p) as f64,
73 dispersion,
74 mcfadden_r2,
75 aic,
76 bic,
77 }
78 }
79}