Skip to main content

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}