1use std::fmt;
4
5use statrs::distribution::{ContinuousCDF, StudentsT};
6
7use crate::coefficients::standardized_coefficients;
8use crate::fit_statistics::{
9 adjusted_r_squared, aic, bic, f_statistic, log_likelihood, r_squared, FStatistic,
10};
11use crate::influence::cooks_distance;
12use crate::multicollinearity::{condition_number, vif};
13use crate::residuals::white_test;
14use crate::residuals::{
15 breusch_pagan, durbin_watson, jarque_bera, BreuschPagan, JarqueBera, WhiteTest,
16};
17use crate::OlsFit;
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct CoefficientRow {
22 pub name: String,
24 pub estimate: f64,
26 pub std_error: f64,
28 pub t_value: f64,
30 pub p_value: f64,
32 pub std_coefficient: f64,
34 pub vif: f64,
36}
37
38#[derive(Debug, Clone)]
47pub struct Summary {
48 pub n_observations: usize,
50 pub n_parameters: usize,
52 pub df_residual: f64,
54 pub has_intercept: bool,
56 pub coefficients: Vec<CoefficientRow>,
58 pub r_squared: f64,
60 pub adj_r_squared: f64,
62 pub f_statistic: FStatistic,
64 pub residual_std_error: f64,
66 pub log_likelihood: f64,
68 pub aic: f64,
70 pub bic: f64,
72 pub condition_number: f64,
74 pub durbin_watson: f64,
76 pub jarque_bera: JarqueBera,
78 pub breusch_pagan: BreuschPagan,
80 pub white: WhiteTest,
82}
83
84impl OlsFit {
85 pub fn summary(&self) -> Summary {
100 let se = self.coefficient_standard_errors();
101 let coef = self.coefficients();
102 let vifs = vif(self);
103 let std_coefs = standardized_coefficients(self);
104 let df = self.df_residual();
105 let t_dist = StudentsT::new(0.0, 1.0, df).ok();
106
107 let mut predictor_counter = 0usize;
108 let coefficients = (0..self.n_parameters())
109 .map(|j| {
110 let name = if self.intercept_column() == Some(j) {
111 "const".to_string()
112 } else {
113 predictor_counter += 1;
114 format!("x{predictor_counter}")
115 };
116 let est = coef[j];
117 let s = se[j];
118 let t = if s > 0.0 { est / s } else { f64::NAN };
119 let p = match &t_dist {
120 Some(d) if t.is_finite() => 2.0 * (1.0 - d.cdf(t.abs())),
121 _ => f64::NAN,
122 };
123 CoefficientRow {
124 name,
125 estimate: est,
126 std_error: s,
127 t_value: t,
128 p_value: p,
129 std_coefficient: std_coefs[j],
130 vif: vifs[j],
131 }
132 })
133 .collect();
134
135 Summary {
136 n_observations: self.n_observations(),
137 n_parameters: self.n_parameters(),
138 df_residual: df,
139 has_intercept: self.has_intercept(),
140 coefficients,
141 r_squared: r_squared(self),
142 adj_r_squared: adjusted_r_squared(self),
143 f_statistic: f_statistic(self),
144 residual_std_error: self.residual_standard_error(),
145 log_likelihood: log_likelihood(self),
146 aic: aic(self),
147 bic: bic(self),
148 condition_number: condition_number(self),
149 durbin_watson: durbin_watson(self),
150 jarque_bera: jarque_bera(self),
151 breusch_pagan: breusch_pagan(self),
152 white: white_test(self),
153 }
154 }
155}
156
157impl Summary {
158 pub fn max_cooks_distance(fit: &OlsFit) -> f64 {
164 cooks_distance(fit)
165 .iter()
166 .copied()
167 .filter(|v| v.is_finite())
168 .fold(0.0_f64, f64::max)
169 }
170}
171
172fn flag(p: f64, low: f64, high: f64, hi_is_bad: bool) -> &'static str {
173 if p.is_nan() {
174 return "";
175 }
176 let bad = if hi_is_bad { p > high } else { p < low };
177 if bad {
178 " (!)"
179 } else {
180 ""
181 }
182}
183
184impl fmt::Display for Summary {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 writeln!(f, "{:=^78}", " OLS Diagnostics ")?;
187 writeln!(
188 f,
189 "No. Observations: {:>6} Df Residuals: {:>6} Df Model: {:>6}",
190 self.n_observations,
191 self.df_residual as usize,
192 self.n_parameters - usize::from(self.has_intercept),
193 )?;
194 writeln!(
195 f,
196 "R-squared: {:>8.4} Adj. R-squared: {:>8.4} Resid. SE: {:>8.4}",
197 self.r_squared, self.adj_r_squared, self.residual_std_error,
198 )?;
199 writeln!(
200 f,
201 "F-statistic: {:>8.4} Prob(F): {:>8.4} Log-Lik: {:>8.2}",
202 self.f_statistic.statistic, self.f_statistic.p_value, self.log_likelihood,
203 )?;
204 writeln!(
205 f,
206 "AIC: {:>8.2} BIC: {:>8.2} Cond. No.: {:>8.3e}",
207 self.aic, self.bic, self.condition_number,
208 )?;
209
210 writeln!(f, "{:-<78}", "")?;
211 writeln!(
212 f,
213 "{:<8}{:>12}{:>11}{:>9}{:>9}{:>9}{:>9}",
214 "", "coef", "std err", "t", "P>|t|", "beta", "VIF",
215 )?;
216 writeln!(f, "{:-<78}", "")?;
217 for row in &self.coefficients {
218 let beta = if row.std_coefficient.is_nan() {
219 " - ".to_string()
220 } else {
221 format!("{:>9.3}", row.std_coefficient)
222 };
223 let vif = if row.vif.is_nan() {
224 " - ".to_string()
225 } else if row.vif.is_infinite() {
226 " inf".to_string()
227 } else {
228 format!("{:>9.2}", row.vif)
229 };
230 writeln!(
231 f,
232 "{:<8}{:>12.4}{:>11.4}{:>9.3}{:>9.3}{beta}{vif}",
233 row.name, row.estimate, row.std_error, row.t_value, row.p_value,
234 )?;
235 }
236 writeln!(f, "{:-<78}", "")?;
237
238 writeln!(
239 f,
240 "Durbin-Watson: {:>8.4} (residual autocorrelation; ~2 is ideal)",
241 self.durbin_watson,
242 )?;
243 writeln!(
244 f,
245 "Jarque-Bera: {:>8.4} Prob: {:>7.4}{} (skew {:.3}, kurt {:.3})",
246 self.jarque_bera.statistic,
247 self.jarque_bera.p_value,
248 flag(self.jarque_bera.p_value, 0.05, 0.0, false),
249 self.jarque_bera.skewness,
250 self.jarque_bera.kurtosis,
251 )?;
252 writeln!(
253 f,
254 "Breusch-Pagan: {:>8.4} Prob: {:>7.4}{} (heteroskedasticity, LM)",
255 self.breusch_pagan.statistic,
256 self.breusch_pagan.p_value,
257 flag(self.breusch_pagan.p_value, 0.05, 0.0, false),
258 )?;
259 writeln!(
260 f,
261 "White: {:>8.4} Prob: {:>7.4}{} (heteroskedasticity, general)",
262 self.white.statistic,
263 self.white.p_value,
264 flag(self.white.p_value, 0.05, 0.0, false),
265 )?;
266 write!(f, "{:=<78}", "")?;
267 Ok(())
268 }
269}