use super::super::{Cdf, LogCdf, Moments, Pdf, Quantile, Sample, bisection_quantile, count_to_f64};
use crate::distributions::FDistribution;
use crate::rng::SplitMix64;
use crate::special::{betai, ln_beta, ln_betai_lower, ln_betai_upper};
impl FDistribution {
fn d1(&self) -> f64 {
count_to_f64(self.numerator_df)
}
fn d2(&self) -> f64 {
count_to_f64(self.denominator_df)
}
}
impl Pdf for FDistribution {
fn pdf(&self, x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
let (d1, d2) = (self.d1(), self.d2());
let log_num = (0.5 * d1).mul_add((d1 * x).ln(), 0.5 * d2 * d2.ln());
let log_den =
(0.5 * (d1 + d2)).mul_add(d1.mul_add(x, d2).ln(), ln_beta(0.5 * d1, 0.5 * d2));
(log_num - log_den - x.ln()).exp()
}
}
impl Cdf for FDistribution {
fn cdf(&self, x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
let (d1, d2) = (self.d1(), self.d2());
let d1x = d1 * x;
betai(0.5 * d1, 0.5 * d2, d1x / (d1x + d2))
}
}
impl LogCdf for FDistribution {
fn logsf(&self, x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
let (d1, d2) = (self.d1(), self.d2());
let d1x = d1 * x;
ln_betai_upper(0.5 * d1, 0.5 * d2, d1x / (d1x + d2))
}
fn logcdf(&self, x: f64) -> f64 {
if x <= 0.0 {
return f64::NEG_INFINITY;
}
let (d1, d2) = (self.d1(), self.d2());
let d1x = d1 * x;
ln_betai_lower(0.5 * d1, 0.5 * d2, d1x / (d1x + d2))
}
}
impl Quantile for FDistribution {
fn quantile(&self, p: f64) -> f64 {
bisection_quantile(p, 0.0, 1.0e7, |x| self.cdf(x))
}
}
impl Moments for FDistribution {
fn mean(&self) -> Option<f64> {
let d2 = self.d2();
if self.denominator_df > 2 {
Some(d2 / (d2 - 2.0))
} else {
None
}
}
fn variance(&self) -> Option<f64> {
if self.denominator_df <= 4 {
return None;
}
let (d1, d2) = (self.d1(), self.d2());
let numerator = 2.0 * d2 * d2 * (d1 + d2 - 2.0);
let denominator = d1 * (d2 - 2.0) * (d2 - 2.0) * (d2 - 4.0);
Some(numerator / denominator)
}
}
impl Sample for FDistribution {
fn sample(&self, rng: &mut SplitMix64) -> f64 {
let num = chi_squared_over_df(self.numerator_df, rng);
let den = chi_squared_over_df(self.denominator_df, rng);
num / den
}
}
fn chi_squared_over_df(df: i64, rng: &mut SplitMix64) -> f64 {
let mut sum = 0.0;
for _ in 0..df.max(1) {
let z = rng.standard_normal();
sum = z.mul_add(z, sum);
}
sum / count_to_f64(df.max(1))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mean_for_large_denominator() {
let d = FDistribution {
numerator_df: 6,
denominator_df: 12,
..Default::default()
};
assert!(matches!(d.mean(), Some(m) if (m - 12.0 / 10.0).abs() < 1e-12));
}
#[test]
fn cdf_saturates() {
let d = FDistribution {
numerator_df: 6,
denominator_df: 12,
..Default::default()
};
assert!(d.cdf(1e6) > 1.0 - 1e-9);
}
#[test]
fn logsf_matches_scipy_and_stays_finite() {
let d = FDistribution {
numerator_df: 3,
denominator_df: 10,
..Default::default()
};
let body = d.logsf(1.0);
assert!(
((body - (1.0 - d.cdf(1.0)).ln()) / body.abs()).abs() < 1e-9,
"logsf body was {body}"
);
let tail = d.logsf(1e4);
let want = -39.037_790_534_655_89; assert!(tail.is_finite(), "logsf(1e4) was {tail}");
assert!(
((tail - want) / want).abs() < 1e-9,
"logsf(1e4) = {tail}, want {want}"
);
}
}