use std::fmt;
use statrs::distribution::{ContinuousCDF, StudentsT};
use crate::coefficients::standardized_coefficients;
use crate::fit_statistics::{
adjusted_r_squared, aic, bic, f_statistic, log_likelihood, r_squared, FStatistic,
};
use crate::influence::cooks_distance;
use crate::multicollinearity::{condition_number, vif};
use crate::residuals::white_test;
use crate::residuals::{
breusch_pagan, durbin_watson, jarque_bera, BreuschPagan, JarqueBera, WhiteTest,
};
use crate::OlsFit;
#[derive(Debug, Clone, PartialEq)]
pub struct CoefficientRow {
pub name: String,
pub estimate: f64,
pub std_error: f64,
pub t_value: f64,
pub p_value: f64,
pub std_coefficient: f64,
pub vif: f64,
}
#[derive(Debug, Clone)]
pub struct Summary {
pub n_observations: usize,
pub n_parameters: usize,
pub df_residual: f64,
pub has_intercept: bool,
pub coefficients: Vec<CoefficientRow>,
pub r_squared: f64,
pub adj_r_squared: f64,
pub f_statistic: FStatistic,
pub residual_std_error: f64,
pub log_likelihood: f64,
pub aic: f64,
pub bic: f64,
pub condition_number: f64,
pub durbin_watson: f64,
pub jarque_bera: JarqueBera,
pub breusch_pagan: BreuschPagan,
pub white: WhiteTest,
}
impl OlsFit {
pub fn summary(&self) -> Summary {
let se = self.coefficient_standard_errors();
let coef = self.coefficients();
let vifs = vif(self);
let std_coefs = standardized_coefficients(self);
let df = self.df_residual();
let t_dist = StudentsT::new(0.0, 1.0, df).ok();
let mut predictor_counter = 0usize;
let coefficients = (0..self.n_parameters())
.map(|j| {
let name = if self.intercept_column() == Some(j) {
"const".to_string()
} else {
predictor_counter += 1;
format!("x{predictor_counter}")
};
let est = coef[j];
let s = se[j];
let t = if s > 0.0 { est / s } else { f64::NAN };
let p = match &t_dist {
Some(d) if t.is_finite() => 2.0 * (1.0 - d.cdf(t.abs())),
_ => f64::NAN,
};
CoefficientRow {
name,
estimate: est,
std_error: s,
t_value: t,
p_value: p,
std_coefficient: std_coefs[j],
vif: vifs[j],
}
})
.collect();
Summary {
n_observations: self.n_observations(),
n_parameters: self.n_parameters(),
df_residual: df,
has_intercept: self.has_intercept(),
coefficients,
r_squared: r_squared(self),
adj_r_squared: adjusted_r_squared(self),
f_statistic: f_statistic(self),
residual_std_error: self.residual_standard_error(),
log_likelihood: log_likelihood(self),
aic: aic(self),
bic: bic(self),
condition_number: condition_number(self),
durbin_watson: durbin_watson(self),
jarque_bera: jarque_bera(self),
breusch_pagan: breusch_pagan(self),
white: white_test(self),
}
}
}
impl Summary {
pub fn max_cooks_distance(fit: &OlsFit) -> f64 {
cooks_distance(fit)
.iter()
.copied()
.filter(|v| v.is_finite())
.fold(0.0_f64, f64::max)
}
}
fn flag(p: f64, low: f64, high: f64, hi_is_bad: bool) -> &'static str {
if p.is_nan() {
return "";
}
let bad = if hi_is_bad { p > high } else { p < low };
if bad {
" (!)"
} else {
""
}
}
impl fmt::Display for Summary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{:=^78}", " OLS Diagnostics ")?;
writeln!(
f,
"No. Observations: {:>6} Df Residuals: {:>6} Df Model: {:>6}",
self.n_observations,
self.df_residual as usize,
self.n_parameters - usize::from(self.has_intercept),
)?;
writeln!(
f,
"R-squared: {:>8.4} Adj. R-squared: {:>8.4} Resid. SE: {:>8.4}",
self.r_squared, self.adj_r_squared, self.residual_std_error,
)?;
writeln!(
f,
"F-statistic: {:>8.4} Prob(F): {:>8.4} Log-Lik: {:>8.2}",
self.f_statistic.statistic, self.f_statistic.p_value, self.log_likelihood,
)?;
writeln!(
f,
"AIC: {:>8.2} BIC: {:>8.2} Cond. No.: {:>8.3e}",
self.aic, self.bic, self.condition_number,
)?;
writeln!(f, "{:-<78}", "")?;
writeln!(
f,
"{:<8}{:>12}{:>11}{:>9}{:>9}{:>9}{:>9}",
"", "coef", "std err", "t", "P>|t|", "beta", "VIF",
)?;
writeln!(f, "{:-<78}", "")?;
for row in &self.coefficients {
let beta = if row.std_coefficient.is_nan() {
" - ".to_string()
} else {
format!("{:>9.3}", row.std_coefficient)
};
let vif = if row.vif.is_nan() {
" - ".to_string()
} else if row.vif.is_infinite() {
" inf".to_string()
} else {
format!("{:>9.2}", row.vif)
};
writeln!(
f,
"{:<8}{:>12.4}{:>11.4}{:>9.3}{:>9.3}{beta}{vif}",
row.name, row.estimate, row.std_error, row.t_value, row.p_value,
)?;
}
writeln!(f, "{:-<78}", "")?;
writeln!(
f,
"Durbin-Watson: {:>8.4} (residual autocorrelation; ~2 is ideal)",
self.durbin_watson,
)?;
writeln!(
f,
"Jarque-Bera: {:>8.4} Prob: {:>7.4}{} (skew {:.3}, kurt {:.3})",
self.jarque_bera.statistic,
self.jarque_bera.p_value,
flag(self.jarque_bera.p_value, 0.05, 0.0, false),
self.jarque_bera.skewness,
self.jarque_bera.kurtosis,
)?;
writeln!(
f,
"Breusch-Pagan: {:>8.4} Prob: {:>7.4}{} (heteroskedasticity, LM)",
self.breusch_pagan.statistic,
self.breusch_pagan.p_value,
flag(self.breusch_pagan.p_value, 0.05, 0.0, false),
)?;
writeln!(
f,
"White: {:>8.4} Prob: {:>7.4}{} (heteroskedasticity, general)",
self.white.statistic,
self.white.p_value,
flag(self.white.p_value, 0.05, 0.0, false),
)?;
write!(f, "{:=<78}", "")?;
Ok(())
}
}