Skip to main content

regression_diagnostics/fit_statistics/
f_statistic.rs

1use statrs::distribution::{ContinuousCDF, FisherSnedecor};
2
3use super::total_sum_of_squares;
4use crate::OlsFit;
5
6/// Result of the overall-significance F-test.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct FStatistic {
9    /// The F statistic.
10    pub statistic: f64,
11    /// Numerator degrees of freedom (model): `p − 1` with an intercept.
12    pub df_model: f64,
13    /// Denominator degrees of freedom (residual): `n − p`.
14    pub df_residual: f64,
15    /// Upper-tail p-value under the F distribution.
16    pub p_value: f64,
17}
18
19/// Overall model-significance F-test, comparing the fitted model against the
20/// intercept-only null.
21///
22/// `F = (explained / df_model) / (RSS / df_residual)`, with `explained = TSS −
23/// RSS`. The p-value is the upper tail of the F distribution with `(df_model,
24/// df_residual)` degrees of freedom.
25///
26/// For a model with no non-intercept predictors (`df_model = 0`) the test is not
27/// defined and the statistic/p-value are returned as `NaN`.
28pub fn f_statistic(fit: &OlsFit) -> FStatistic {
29    let df_model = fit.df_model();
30    let df_residual = fit.df_residual();
31    let tss = total_sum_of_squares(fit);
32    let rss = fit.residual_sum_of_squares();
33    let explained = tss - rss;
34
35    if df_model <= 0.0 || df_residual <= 0.0 || rss <= 0.0 {
36        return FStatistic {
37            statistic: f64::NAN,
38            df_model,
39            df_residual,
40            p_value: f64::NAN,
41        };
42    }
43
44    let statistic = (explained / df_model) / (rss / df_residual);
45    let p_value = match FisherSnedecor::new(df_model, df_residual) {
46        Ok(dist) if statistic.is_finite() && statistic >= 0.0 => 1.0 - dist.cdf(statistic),
47        _ => f64::NAN,
48    };
49
50    FStatistic {
51        statistic,
52        df_model,
53        df_residual,
54        p_value,
55    }
56}