1use greeners_core::error::GreenersError;
2use greeners_core::linalg::{LinalgInverse as _, LinalgQR as _};
3use greeners_core::{CovarianceType, InferenceType};
4use greeners_core::{DataFrame, Formula};
5use ndarray::{Array1, Array2};
6use statrs::distribution::{ContinuousCDF, FisherSnedecor, Normal, StudentsT};
7use std::fmt;
8
9type InferenceResult = (Array1<f64>, Array1<f64>, Array1<f64>);
11
12#[derive(Debug, Clone)]
14pub struct PredictionResult {
15 pub mean: Array1<f64>,
16 pub se: Array1<f64>,
17 pub ci_lower: Array1<f64>,
18 pub ci_upper: Array1<f64>,
19}
20
21#[derive(Debug, Clone)]
22pub struct OlsResult {
23 pub params: Array1<f64>,
24 pub std_errors: Array1<f64>,
25 pub t_values: Array1<f64>,
26 pub p_values: Array1<f64>,
27 pub conf_lower: Array1<f64>,
28 pub conf_upper: Array1<f64>,
29 pub r_squared: f64,
30 pub adj_r_squared: f64,
31 pub f_statistic: f64,
32 pub prob_f: f64,
33 pub log_likelihood: f64,
34 pub aic: f64,
35 pub bic: f64,
36 pub n_obs: usize,
37 pub df_resid: usize,
38 pub df_model: usize,
39 pub sigma: f64,
40 pub cov_type: CovarianceType, pub inference_type: InferenceType, pub variable_names: Option<Vec<String>>, pub omitted_vars: Vec<(usize, String)>, pub x_clean: Option<Array2<f64>>, }
46
47impl OlsResult {
48 pub fn predict(&self, x_new: &Array2<f64>) -> Array1<f64> {
71 x_new.dot(&self.params)
72 }
73
74 pub fn residuals(&self, y: &Array1<f64>, x: &Array2<f64>) -> Array1<f64> {
83 let y_hat = x.dot(&self.params);
84 y - &y_hat
85 }
86
87 pub fn fitted_values(&self, x: &Array2<f64>) -> Array1<f64> {
95 x.dot(&self.params)
96 }
97
98 pub fn model_stats(&self) -> (f64, f64, f64, f64) {
121 (self.aic, self.bic, self.log_likelihood, self.adj_r_squared)
122 }
123
124 pub fn partial_r_squared(&self, indices: &[usize], y: &Array1<f64>, x: &Array2<f64>) -> f64 {
139 let fitted_full = self.fitted_values(x);
141 let resid_full = y - &fitted_full;
142 let ssr_full = resid_full.dot(&resid_full);
143
144 let n = x.nrows();
146 let k_full = x.ncols();
147 let k_restricted = k_full - indices.len();
148
149 if k_restricted == 0 {
150 return self.r_squared; }
152
153 let mut x_restricted = Array2::<f64>::zeros((n, k_restricted));
155 let mut col_idx = 0;
156 for j in 0..k_full {
157 if !indices.contains(&j) {
158 x_restricted.column_mut(col_idx).assign(&x.column(j));
159 col_idx += 1;
160 }
161 }
162
163 use greeners_core::linalg::LinalgInverse as _;
165 let xt_x = x_restricted.t().dot(&x_restricted);
166 let xt_y = x_restricted.t().dot(y);
167
168 if let Ok(xt_x_inv) = xt_x.inv() {
169 let beta_restricted = xt_x_inv.dot(&xt_y);
170 let fitted_restricted = x_restricted.dot(&beta_restricted);
171 let resid_restricted = y - &fitted_restricted;
172 let ssr_restricted = resid_restricted.dot(&resid_restricted);
173
174 (ssr_restricted - ssr_full) / ssr_restricted
176 } else {
177 0.0 }
179 }
180
181 pub fn conf_int(&self, alpha: f64) -> Result<Vec<(f64, f64)>, GreenersError> {
185 let critical_value = match self.inference_type {
186 InferenceType::StudentT => {
187 let t_dist = StudentsT::new(0.0, 1.0, self.df_resid as f64)
188 .map_err(|_| GreenersError::OptimizationFailed)?;
189 t_dist.inverse_cdf(1.0 - alpha / 2.0)
190 }
191 InferenceType::Normal => {
192 let normal_dist =
193 Normal::new(0.0, 1.0).map_err(|_| GreenersError::OptimizationFailed)?;
194 normal_dist.inverse_cdf(1.0 - alpha / 2.0)
195 }
196 };
197
198 Ok((0..self.params.len())
199 .map(|i| {
200 let margin = self.std_errors[i] * critical_value;
201 (self.params[i] - margin, self.params[i] + margin)
202 })
203 .collect())
204 }
205
206 pub fn get_prediction(
213 &self,
214 x_new: &Array2<f64>,
215 x_orig: &Array2<f64>,
216 alpha: f64,
217 ) -> Result<PredictionResult, GreenersError> {
218 let mean = x_new.dot(&self.params);
219
220 let xt_x = x_orig.t().dot(x_orig);
222 let xt_x_inv = xt_x.inv()?;
223
224 let sigma2 = self.sigma * self.sigma;
225
226 let n_pred = x_new.nrows();
228 let mut se = Array1::<f64>::zeros(n_pred);
229 for i in 0..n_pred {
230 let xi = x_new.row(i);
231 let var_i = xi.dot(&xt_x_inv.dot(&xi)) * sigma2;
232 se[i] = var_i.max(0.0).sqrt();
233 }
234
235 let critical_value = match self.inference_type {
236 InferenceType::StudentT => {
237 let t_dist = StudentsT::new(0.0, 1.0, self.df_resid as f64)
238 .map_err(|_| GreenersError::OptimizationFailed)?;
239 t_dist.inverse_cdf(1.0 - alpha / 2.0)
240 }
241 InferenceType::Normal => {
242 let normal_dist =
243 Normal::new(0.0, 1.0).map_err(|_| GreenersError::OptimizationFailed)?;
244 normal_dist.inverse_cdf(1.0 - alpha / 2.0)
245 }
246 };
247
248 let margin = &se * critical_value;
249 let ci_lower = &mean - &margin;
250 let ci_upper = &mean + &margin;
251
252 Ok(PredictionResult {
253 mean,
254 se,
255 ci_lower,
256 ci_upper,
257 })
258 }
259
260 pub fn wald_test(
267 &self,
268 r_matrix: &Array2<f64>,
269 q: &Array1<f64>,
270 x: &Array2<f64>,
271 ) -> Result<(f64, f64), GreenersError> {
272 let j = r_matrix.nrows();
273 let rb = r_matrix.dot(&self.params);
274 let diff = &rb - q;
275
276 let xt_x = x.t().dot(x);
279 let xt_x_inv = xt_x.inv()?;
280 let sigma2 = self.sigma * self.sigma;
281 let cov = &xt_x_inv * sigma2;
282
283 let r_cov_r = r_matrix.dot(&cov).dot(&r_matrix.t());
284 let r_cov_r_inv = r_cov_r.inv()?;
285
286 let wald_stat = diff.dot(&r_cov_r_inv.dot(&diff)) / j as f64;
287
288 let f_dist = FisherSnedecor::new(j as f64, self.df_resid as f64)
289 .map_err(|_| GreenersError::OptimizationFailed)?;
290 let p_value = 1.0 - f_dist.cdf(wald_stat);
291
292 Ok((wald_stat, p_value))
293 }
294
295 pub fn f_test(&self, indices: &[usize], x: &Array2<f64>) -> Result<(f64, f64), GreenersError> {
299 let j = indices.len();
300 let k = self.params.len();
301
302 let mut r_matrix = Array2::<f64>::zeros((j, k));
303 for (row, &col) in indices.iter().enumerate() {
304 r_matrix[[row, col]] = 1.0;
305 }
306 let q = Array1::<f64>::zeros(j);
307
308 self.wald_test(&r_matrix, &q, x)
309 }
310
311 pub fn t_test(
315 &self,
316 r_vector: &Array1<f64>,
317 q: f64,
318 x: &Array2<f64>,
319 ) -> Result<(f64, f64), GreenersError> {
320 let rb = r_vector.dot(&self.params);
321 let diff = rb - q;
322
323 let xt_x = x.t().dot(x);
324 let xt_x_inv = xt_x.inv()?;
325 let sigma2 = self.sigma * self.sigma;
326 let cov = &xt_x_inv * sigma2;
327
328 let se = r_vector.dot(&cov.dot(r_vector)).max(0.0).sqrt();
329 if se < 1e-15 {
330 return Err(GreenersError::InvalidOperation(
331 "Standard error is zero".into(),
332 ));
333 }
334
335 let t_stat = diff / se;
336
337 let p_value = if t_stat.is_nan() {
338 f64::NAN
339 } else if !t_stat.is_finite() {
340 0.0
341 } else {
342 match self.inference_type {
343 InferenceType::StudentT => {
344 let t_dist = StudentsT::new(0.0, 1.0, self.df_resid as f64)
345 .map_err(|_| GreenersError::OptimizationFailed)?;
346 2.0 * (1.0 - t_dist.cdf(t_stat.abs()))
347 }
348 InferenceType::Normal => {
349 let normal_dist =
350 Normal::new(0.0, 1.0).map_err(|_| GreenersError::OptimizationFailed)?;
351 2.0 * (1.0 - normal_dist.cdf(t_stat.abs()))
352 }
353 }
354 };
355
356 Ok((t_stat, p_value))
357 }
358
359 pub fn nlcom<F>(&self, g: F, x: &Array2<f64>) -> Result<(f64, f64, f64, f64), GreenersError>
371 where
372 F: Fn(&[f64]) -> f64,
373 {
374 let params = self.params.as_slice().ok_or_else(|| {
375 GreenersError::InvalidOperation("Non-contiguous parameter array".to_string())
376 })?;
377 let k = params.len();
378 let g_hat = g(params);
379
380 let h = 1e-7;
382 let mut grad = Array1::<f64>::zeros(k);
383 let mut perturbed = params.to_vec();
384 for j in 0..k {
385 let orig = perturbed[j];
386 perturbed[j] = orig + h;
387 let g_plus = g(&perturbed);
388 perturbed[j] = orig - h;
389 let g_minus = g(&perturbed);
390 grad[j] = (g_plus - g_minus) / (2.0 * h);
391 perturbed[j] = orig;
392 }
393
394 let xt_x = x.t().dot(x);
396 let xt_x_inv = xt_x.inv()?;
397 let sigma2 = self.sigma * self.sigma;
398 let vcov = &xt_x_inv * sigma2;
399
400 let se = grad.dot(&vcov.dot(&grad)).max(0.0).sqrt();
402 let t = if se > 1e-15 { g_hat / se } else { f64::NAN };
403 let p = greeners_core::t_pvalue_two(t, self.df_resid as f64);
404
405 Ok((g_hat, se, t, p))
406 }
407
408 pub fn compute_inference(
423 t_values: &Array1<f64>,
424 std_errors: &Array1<f64>,
425 params: &Array1<f64>,
426 df_resid: usize,
427 inference_type: &InferenceType,
428 ) -> Result<InferenceResult, GreenersError> {
429 let (p_values, critical_value) = match inference_type {
430 InferenceType::StudentT => {
431 let t_dist = StudentsT::new(0.0, 1.0, df_resid as f64)
432 .map_err(|_| GreenersError::OptimizationFailed)?;
433 let p_vals = t_values.mapv(|t| {
434 if t.is_nan() {
435 f64::NAN
436 } else if !t.is_finite() {
437 0.0
438 } else {
439 2.0 * (1.0 - t_dist.cdf(t.abs()))
440 }
441 });
442 (p_vals, t_dist.inverse_cdf(0.975))
443 }
444 InferenceType::Normal => {
445 let normal_dist =
446 Normal::new(0.0, 1.0).map_err(|_| GreenersError::OptimizationFailed)?;
447 let p_vals = t_values.mapv(|t| {
448 if t.is_nan() {
449 f64::NAN
450 } else if !t.is_finite() {
451 0.0
452 } else {
453 2.0 * (1.0 - normal_dist.cdf(t.abs()))
454 }
455 });
456 (p_vals, normal_dist.inverse_cdf(0.975))
457 }
458 };
459
460 let margin_error = std_errors * critical_value;
461 let conf_lower = params - &margin_error;
462 let conf_upper = params + &margin_error;
463
464 Ok((p_values, conf_lower, conf_upper))
465 }
466
467 pub fn with_inference(mut self, inference_type: InferenceType) -> Result<Self, GreenersError> {
501 let (p_values, conf_lower, conf_upper) = Self::compute_inference(
502 &self.t_values,
503 &self.std_errors,
504 &self.params,
505 self.df_resid,
506 &inference_type,
507 )?;
508
509 self.p_values = p_values;
510 self.conf_lower = conf_lower;
511 self.conf_upper = conf_upper;
512 self.inference_type = inference_type;
513
514 Ok(self)
515 }
516}
517
518impl fmt::Display for OlsResult {
519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520 let stat_label = match self.inference_type {
521 InferenceType::StudentT => "t",
522 InferenceType::Normal => "z",
523 };
524
525 let cov_str = match &self.cov_type {
526 CovarianceType::NonRobust => "Non-Robust".to_string(),
527 CovarianceType::HC1 => "Robust (HC1)".to_string(),
528 CovarianceType::HC2 => "Robust (HC2)".to_string(),
529 CovarianceType::HC3 => "Robust (HC3)".to_string(),
530 CovarianceType::HC4 => "Robust (HC4)".to_string(),
531 CovarianceType::NeweyWest(lags) => format!("HAC (Newey-West, L={})", lags),
532 CovarianceType::Clustered(clusters) => {
533 let n_clusters = clusters
534 .iter()
535 .collect::<std::collections::HashSet<_>>()
536 .len();
537 format!("Clustered ({} clusters)", n_clusters)
538 }
539 CovarianceType::ClusteredTwoWay(clusters1, clusters2) => {
540 let n_clusters_1 = clusters1
541 .iter()
542 .collect::<std::collections::HashSet<_>>()
543 .len();
544 let n_clusters_2 = clusters2
545 .iter()
546 .collect::<std::collections::HashSet<_>>()
547 .len();
548 format!("Two-Way Clustered ({}×{})", n_clusters_1, n_clusters_2)
549 }
550 };
551
552 writeln!(f, "\n{:=^78}", " OLS Regression Results ")?;
553 writeln!(
554 f,
555 "{:<20} {:>15} || {:<20} {:>15.4}",
556 "Dep. Variable:", "y", "R-squared:", self.r_squared
557 )?;
558 writeln!(
559 f,
560 "{:<20} {:>15} || {:<20} {:>15.4}",
561 "Model:", "OLS", "Adj. R-squared:", self.adj_r_squared
562 )?;
563 let f_str = if self.f_statistic.is_infinite() {
564 "Inf".to_string()
565 } else {
566 format!("{:.4}", self.f_statistic)
567 };
568 let prob_f_str = if self.f_statistic.is_infinite() {
569 "0.0".to_string()
570 } else {
571 format!("{:.4e}", self.prob_f)
572 };
573 writeln!(
574 f,
575 "{:<20} {:>15} || {:<20} {:>15}",
576 "Covariance Type:", cov_str, "F-statistic:", f_str
577 )?;
578 writeln!(
579 f,
580 "{:<20} {:>15} || {:<20} {:>15}",
581 "No. Observations:", self.n_obs, "Prob (F-statistic):", prob_f_str
582 )?;
583 writeln!(
584 f,
585 "{:<20} {:>15} || {:<20} {:>15.4}",
586 "Df Residuals:", self.df_resid, "Log-Likelihood:", self.log_likelihood
587 )?;
588 writeln!(
589 f,
590 "{:<20} {:>15.4} || {:<20} {:>15.4}",
591 "AIC:", self.aic, "BIC:", self.bic
592 )?;
593
594 writeln!(f, "\n{:-^78}", "")?;
595 writeln!(
596 f,
597 "{:<10} | {:>10} | {:>10} | {:>8} | {:>8} | {:>18}",
598 "Variable",
599 "coef",
600 "std err",
601 stat_label,
602 format!("P>|{}|", stat_label),
603 "[0.025 0.975]"
604 )?;
605 writeln!(f, "{:-^78}", "")?;
606
607 let total = self.params.len() + self.omitted_vars.len();
608 let mut fit_idx = 0usize;
609 for pos in 0..total {
610 if let Some((_, name)) = self.omitted_vars.iter().find(|(p, _)| *p == pos) {
611 writeln!(f, "{:<10} | (omitted)", name)?;
612 } else {
613 let var_name = if let Some(ref names) = self.variable_names {
614 if fit_idx < names.len() {
615 names[fit_idx].clone()
616 } else {
617 format!("x{}", fit_idx)
618 }
619 } else {
620 format!("x{}", fit_idx)
621 };
622 let t_val = self.t_values[fit_idx];
623 let t_str = if t_val.abs() > 1e10 {
624 format!("{:.3e}", t_val)
625 } else {
626 format!("{:.3}", t_val)
627 };
628 writeln!(
629 f,
630 "{:<10} | {:>10.4} | {:>10.4} | {:>8} | {:>8.3} | {:>8.4} {:>8.4}",
631 var_name,
632 self.params[fit_idx],
633 self.std_errors[fit_idx],
634 t_str,
635 self.p_values[fit_idx],
636 self.conf_lower[fit_idx],
637 self.conf_upper[fit_idx]
638 )?;
639 fit_idx += 1;
640 }
641 }
642
643 writeln!(f, "{:=^78}", "")?;
644 for (_, name) in &self.omitted_vars {
645 writeln!(f, "note: {} omitted because of collinearity", name)?;
646 }
647 Ok(())
648 }
649}
650
651pub struct OLS;
652
653impl OLS {
654 pub fn from_formula(
675 formula: &Formula,
676 data: &DataFrame,
677 cov_type: CovarianceType,
678 ) -> Result<OlsResult, GreenersError> {
679 let (y, x) = data.to_design_matrix(formula)?;
680 let var_names = data.formula_var_names(formula)?;
681 Self::fit_with_names(&y, &x, cov_type, Some(var_names))
682 }
683
684 pub fn detect_collinearity(
688 x: &Array2<f64>,
689 tolerance: f64,
690 ) -> (Array2<f64>, Vec<usize>, Vec<usize>) {
691 let n = x.nrows();
692 let k = x.ncols();
693
694 match x.qr() {
697 Ok((_, r)) => {
698 let mut keep_indices = Vec::new();
699 let mut omit_indices = Vec::new();
700
701 for i in 0..k.min(n) {
703 let r_ii = r[[i, i]].abs();
704 if r_ii > tolerance {
705 keep_indices.push(i);
706 } else {
707 omit_indices.push(i);
708 }
709 }
710
711 if omit_indices.is_empty() {
713 return (x.clone(), keep_indices, omit_indices);
714 }
715
716 let x_clean = x.select(ndarray::Axis(1), &keep_indices);
718 (x_clean, keep_indices, omit_indices)
719 }
720 Err(_) => {
721 let keep: Vec<usize> = (0..k).collect();
723 (x.clone(), keep, vec![])
724 }
725 }
726 }
727
728 pub fn fit(
730 y: &Array1<f64>,
731 x: &Array2<f64>,
732 cov_type: CovarianceType,
733 ) -> Result<OlsResult, GreenersError> {
734 Self::fit_with_names(y, x, cov_type, None)
735 }
736
737 pub fn fit_with_names(
739 y: &Array1<f64>,
740 x: &Array2<f64>,
741 cov_type: CovarianceType,
742 variable_names: Option<Vec<String>>,
743 ) -> Result<OlsResult, GreenersError> {
744 Self::fit_internal(y, x, cov_type, variable_names, None)
745 }
746
747 pub(crate) fn fit_internal(
748 y: &Array1<f64>,
749 x: &Array2<f64>,
750 cov_type: CovarianceType,
751 variable_names: Option<Vec<String>>,
752 force_intercept: Option<bool>,
753 ) -> Result<OlsResult, GreenersError> {
754 let n = x.nrows();
755 let k = x.ncols();
756
757 if y.len() != n {
758 return Err(GreenersError::ShapeMismatch(format!(
759 "y: {}, X: {}",
760 y.len(),
761 n
762 )));
763 }
764 if variable_names.is_none() && n <= k {
765 return Err(GreenersError::ShapeMismatch(
766 "Degrees of freedom <= 0".into(),
767 ));
768 }
769
770 if y.iter().any(|v| !v.is_finite()) || x.iter().any(|v| !v.is_finite()) {
772 return Err(GreenersError::InvalidOperation(
773 "Input data contains NaN or Inf values".into(),
774 ));
775 }
776
777 let (x_to_use, k_clean, omitted_positioned, clean_var_names, x_clean_out) =
778 if let Some(ref names) = variable_names {
779 let cr = greeners_core::linalg::drop_collinear(x, names, 1e-10);
780 let k_clean = cr.x_clean.ncols();
781 if n <= k_clean {
782 return Err(GreenersError::ShapeMismatch(
783 "Degrees of freedom <= 0 after removing collinear variables".into(),
784 ));
785 }
786 let has_omitted = !cr.omitted.is_empty();
787 let x_c = cr.x_clean;
788 (
789 x_c.clone(),
790 k_clean,
791 cr.omitted,
792 cr.clean_names,
793 if has_omitted { Some(x_c) } else { None },
794 )
795 } else {
796 (x.clone(), k, Vec::new(), Vec::new(), None)
797 };
798
799 let x_to_use = &x_to_use;
800
801 let x_t = x_to_use.t();
803 let xt_x = x_t.dot(x_to_use);
804 let xt_x_inv = xt_x.inv()?;
805 let xt_y = x_t.dot(y);
806 let beta = xt_x_inv.dot(&xt_y);
807
808 let predicted = x_to_use.dot(&beta);
810 let residuals = y - &predicted;
811 let ssr = residuals.dot(&residuals);
812
813 let has_intercept = force_intercept.unwrap_or_else(|| {
814 (0..k_clean).any(|j| {
815 x_to_use
816 .column(j)
817 .iter()
818 .all(|&val| (val - 1.0).abs() < 1e-12)
819 })
820 });
821
822 let df_resid = n - k_clean;
823 let df_model = if has_intercept { k_clean - 1 } else { k_clean };
824
825 let sigma2 = ssr / (df_resid as f64);
826 let sigma = sigma2.sqrt();
827
828 let cov_matrix = match &cov_type {
832 CovarianceType::NonRobust => &xt_x_inv * sigma2,
833 CovarianceType::HC1 => {
834 let u_squared = residuals.mapv(|r| r.powi(2));
837 let mut x_weighted = x_to_use.clone();
838 for (i, mut row) in x_weighted.axis_iter_mut(nd::Axis(0)).enumerate() {
839 row *= u_squared[i];
840 }
841 let meat = x_t.dot(&x_weighted);
842 let bread = &xt_x_inv;
843 let sandwich = bread.dot(&meat).dot(bread);
844
845 let correction = (n as f64) / (df_resid as f64);
846 sandwich * correction
847 }
848 CovarianceType::HC2 => {
849 let mut leverage = Array1::<f64>::zeros(n);
855 for i in 0..n {
856 let x_i = x_to_use.row(i);
857 let temp = xt_x_inv.dot(&x_i);
858 leverage[i] = x_i.dot(&temp);
859 }
860
861 let mut u_adjusted = Array1::<f64>::zeros(n);
863 for i in 0..n {
864 let h_i = leverage[i];
865 if h_i >= 0.9999 {
866 u_adjusted[i] = residuals[i].powi(2); } else {
868 u_adjusted[i] = residuals[i].powi(2) / (1.0 - h_i);
869 }
870 }
871
872 let mut x_weighted = x_to_use.clone();
874 for (i, mut row) in x_weighted.axis_iter_mut(nd::Axis(0)).enumerate() {
875 row *= u_adjusted[i];
876 }
877
878 let meat = x_t.dot(&x_weighted);
879 let bread = &xt_x_inv;
880 bread.dot(&meat).dot(bread)
881 }
882 CovarianceType::HC3 => {
883 let mut leverage = Array1::<f64>::zeros(n);
889 for i in 0..n {
890 let x_i = x_to_use.row(i);
891 let temp = xt_x_inv.dot(&x_i);
892 leverage[i] = x_i.dot(&temp);
893 }
894
895 let mut u_adjusted = Array1::<f64>::zeros(n);
897 for i in 0..n {
898 let h_i = leverage[i];
899 if h_i >= 0.9999 {
900 u_adjusted[i] = residuals[i].powi(2); } else {
902 u_adjusted[i] = residuals[i].powi(2) / (1.0 - h_i).powi(2);
903 }
904 }
905
906 let mut x_weighted = x_to_use.clone();
908 for (i, mut row) in x_weighted.axis_iter_mut(nd::Axis(0)).enumerate() {
909 row *= u_adjusted[i];
910 }
911
912 let meat = x_t.dot(&x_weighted);
913 let bread = &xt_x_inv;
914 bread.dot(&meat).dot(bread)
915 }
916 CovarianceType::HC4 => {
917 let mut leverage = Array1::<f64>::zeros(n);
924 for i in 0..n {
925 let x_i = x_to_use.row(i);
926 let temp = xt_x_inv.dot(&x_i);
927 leverage[i] = x_i.dot(&temp);
928 }
929
930 let mut u_adjusted = Array1::<f64>::zeros(n);
932 for i in 0..n {
933 let h_i = leverage[i];
934 if h_i >= 0.9999 {
935 u_adjusted[i] = residuals[i].powi(2);
936 } else {
937 let delta_i = 4.0_f64.min((n as f64) * h_i / (k_clean as f64));
939 u_adjusted[i] = residuals[i].powi(2) / (1.0 - h_i).powf(delta_i);
940 }
941 }
942
943 let mut x_weighted = x_to_use.clone();
945 for (i, mut row) in x_weighted.axis_iter_mut(nd::Axis(0)).enumerate() {
946 row *= u_adjusted[i];
947 }
948
949 let meat = x_t.dot(&x_weighted);
950 let bread = &xt_x_inv;
951 bread.dot(&meat).dot(bread)
952 }
953 CovarianceType::NeweyWest(lags) => {
954 let u_squared = residuals.mapv(|r| r.powi(2));
959 let mut x_weighted = x_to_use.clone();
960 for (i, mut row) in x_weighted.axis_iter_mut(nd::Axis(0)).enumerate() {
961 row *= u_squared[i];
962 }
963 let mut meat = x_t.dot(&x_weighted); for l in 1..=*lags {
968 let weight = 1.0 - (l as f64) / ((*lags + 1) as f64);
969
970 let mut omega_l = Array2::<f64>::zeros((k_clean, k_clean));
975
976 for t in l..n {
978 let u_t = residuals[t];
979 let u_prev = residuals[t - l];
980
981 let x_row_t = x_to_use.row(t);
982 let x_row_prev = x_to_use.row(t - l);
983
984 let scale = u_t * u_prev;
990
991 for i in 0..k_clean {
993 for j in 0..k_clean {
994 omega_l[[i, j]] += scale * x_row_t[i] * x_row_prev[j];
995 }
996 }
997 }
998
999 let omega_l_t = omega_l.t();
1002 let term = &omega_l + &omega_l_t;
1003 meat = meat + (&term * weight);
1004 }
1005
1006 let bread = &xt_x_inv;
1007 let sandwich = bread.dot(&meat).dot(bread);
1008
1009 let correction = (n as f64) / (df_resid as f64);
1011 sandwich * correction
1012 }
1013 CovarianceType::Clustered(ref cluster_ids) => {
1014 if cluster_ids.len() != n {
1020 return Err(GreenersError::ShapeMismatch(format!(
1021 "Cluster IDs length ({}) must match number of observations ({})",
1022 cluster_ids.len(),
1023 n
1024 )));
1025 }
1026
1027 use indexmap::IndexMap;
1029 let mut clusters: IndexMap<usize, Vec<usize>> = IndexMap::new();
1030 for (obs_idx, &cluster_id) in cluster_ids.iter().enumerate() {
1031 clusters.entry(cluster_id).or_default().push(obs_idx);
1032 }
1033
1034 let n_clusters = clusters.len();
1035
1036 let mut meat = Array2::<f64>::zeros((k_clean, k_clean));
1038
1039 for (_cluster_id, obs_indices) in clusters.iter() {
1042 let mut v = Array1::<f64>::zeros(k_clean);
1043 for &obs_idx in obs_indices.iter() {
1044 let r = residuals[obs_idx];
1045 let x_i = x_to_use.row(obs_idx);
1046 for p in 0..k_clean {
1047 v[p] += r * x_i[p];
1048 }
1049 }
1050
1051 for p in 0..k_clean {
1053 let vp = v[p];
1054 let mut meat_row = meat.row_mut(p);
1055 for q in 0..k_clean {
1056 meat_row[q] += vp * v[q];
1057 }
1058 }
1059 }
1060
1061 let bread = &xt_x_inv;
1063 let sandwich = bread.dot(&meat).dot(bread);
1064
1065 let g_correction = (n_clusters as f64) / ((n_clusters - 1) as f64);
1068 let df_correction = ((n - 1) as f64) / (df_resid as f64);
1069 sandwich * g_correction * df_correction
1070 }
1071 CovarianceType::ClusteredTwoWay(ref cluster_ids_1, ref cluster_ids_2) => {
1072 if cluster_ids_1.len() != n || cluster_ids_2.len() != n {
1084 return Err(GreenersError::ShapeMismatch(format!(
1085 "Both cluster ID vectors must match number of observations ({})",
1086 n
1087 )));
1088 }
1089
1090 let compute_clustered_meat = |cluster_ids: &[usize]| -> Array2<f64> {
1092 use indexmap::IndexMap;
1093 let mut clusters: IndexMap<usize, Vec<usize>> = IndexMap::new();
1094 for (obs_idx, &cluster_id) in cluster_ids.iter().enumerate() {
1095 clusters.entry(cluster_id).or_default().push(obs_idx);
1096 }
1097
1098 let mut meat = Array2::<f64>::zeros((k_clean, k_clean));
1099
1100 for (_cluster_id, obs_indices) in clusters.iter() {
1101 let cluster_size = obs_indices.len();
1102 let mut x_g = Array2::<f64>::zeros((cluster_size, k_clean));
1103 let mut u_g = Array1::<f64>::zeros(cluster_size);
1104
1105 for (i, &obs_idx) in obs_indices.iter().enumerate() {
1106 x_g.row_mut(i).assign(&x_to_use.row(obs_idx));
1107 u_g[i] = residuals[obs_idx];
1108 }
1109
1110 for i in 0..cluster_size {
1111 for j in 0..cluster_size {
1112 let scale = u_g[i] * u_g[j];
1113 let x_i = x_g.row(i);
1114 let x_j = x_g.row(j);
1115
1116 for p in 0..k_clean {
1117 for q in 0..k_clean {
1118 meat[[p, q]] += scale * x_i[p] * x_j[q];
1119 }
1120 }
1121 }
1122 }
1123 }
1124
1125 meat
1126 };
1127
1128 let meat_1 = compute_clustered_meat(cluster_ids_1);
1130
1131 let meat_2 = compute_clustered_meat(cluster_ids_2);
1133
1134 let max_cluster2 = cluster_ids_2.iter().max().unwrap_or(&0) + 1;
1137 let intersection_ids: Vec<usize> = cluster_ids_1
1138 .iter()
1139 .zip(cluster_ids_2.iter())
1140 .map(|(&c1, &c2)| c1 * max_cluster2 + c2)
1141 .collect();
1142
1143 let meat_12 = compute_clustered_meat(&intersection_ids);
1144
1145 let meat = &meat_1 + &meat_2 - &meat_12;
1147
1148 let bread = &xt_x_inv;
1150 let sandwich = bread.dot(&meat).dot(bread);
1151
1152 use std::collections::HashSet;
1155 let n_clusters_1: HashSet<_> = cluster_ids_1.iter().collect();
1156 let n_clusters_2: HashSet<_> = cluster_ids_2.iter().collect();
1157 let g = n_clusters_1.len().min(n_clusters_2.len());
1158
1159 let g_correction = (g as f64) / ((g - 1) as f64);
1160 let df_correction = ((n - 1) as f64) / (df_resid as f64);
1161 sandwich * g_correction * df_correction
1162 }
1163 };
1164
1165 let std_errors = cov_matrix.diag().mapv(|v| v.max(0.0).sqrt());
1167 let t_values = &beta / &std_errors;
1168
1169 let default_inference = InferenceType::default();
1171 let (p_values, conf_lower, conf_upper) = OlsResult::compute_inference(
1172 &t_values,
1173 &std_errors,
1174 &beta,
1175 df_resid,
1176 &default_inference,
1177 )?;
1178
1179 let sst = if has_intercept {
1181 let y_mean = y.mean().unwrap_or(0.0);
1182 y.mapv(|val| (val - y_mean).powi(2)).sum()
1183 } else {
1184 y.mapv(|val| val.powi(2)).sum()
1185 };
1186
1187 let r_squared = if sst.abs() < 1e-12 {
1188 0.0
1189 } else {
1190 1.0 - (ssr / sst)
1191 };
1192
1193 let adj_r_squared = if has_intercept {
1194 1.0 - (1.0 - r_squared) * ((n as f64 - 1.0) / (df_resid as f64))
1195 } else {
1196 1.0 - (1.0 - r_squared) * ((n as f64) / (df_resid as f64))
1197 };
1198
1199 let msm = (sst - ssr) / (df_model as f64);
1200 let f_statistic = if sigma2 < 1e-12 {
1201 f64::INFINITY
1202 } else {
1203 msm / sigma2
1204 };
1205
1206 let prob_f = if df_model > 0 && f_statistic.is_finite() {
1207 let f_dist = FisherSnedecor::new(df_model as f64, df_resid as f64)
1208 .map_err(|_| GreenersError::OptimizationFailed)?;
1209 1.0 - f_dist.cdf(f_statistic)
1210 } else if f_statistic.is_infinite() {
1211 0.0
1212 } else {
1213 f64::NAN
1214 };
1215
1216 let n_f64 = n as f64;
1217 let log_likelihood =
1218 -n_f64 / 2.0 * ((2.0 * std::f64::consts::PI).ln() + (ssr / n_f64).ln() + 1.0);
1219 let aic = 2.0 * (k_clean as f64) - 2.0 * log_likelihood;
1220 let bic = (k_clean as f64) * n_f64.ln() - 2.0 * log_likelihood;
1221
1222 Ok(OlsResult {
1223 params: beta,
1224 std_errors,
1225 t_values,
1226 p_values,
1227 conf_lower,
1228 conf_upper,
1229 r_squared,
1230 adj_r_squared,
1231 f_statistic,
1232 prob_f,
1233 log_likelihood,
1234 aic,
1235 bic,
1236 n_obs: n,
1237 df_resid,
1238 df_model,
1239 sigma,
1240 cov_type,
1241 inference_type: InferenceType::default(),
1242 variable_names: if !clean_var_names.is_empty() {
1243 Some(clean_var_names)
1244 } else {
1245 variable_names
1246 },
1247 omitted_vars: omitted_positioned,
1248 x_clean: x_clean_out,
1249 })
1250 }
1251}
1252
1253use ndarray as nd;