1use greeners_core::linalg::LinalgInverse as _;
2use greeners_core::{CovarianceType, DataFrame, Formula, GreenersError, InferenceType};
3use greeners_ols::ols::OLS;
4use indexmap::IndexMap;
5use ndarray::{Array1, Array2, Axis};
6use std::fmt;
7use std::hash::Hash;
8
9#[derive(Debug)]
15pub struct PanelResult {
16 pub params: Array1<f64>,
17 pub std_errors: Array1<f64>,
18 pub t_values: Array1<f64>,
19 pub p_values: Array1<f64>,
20 pub r_squared: f64, pub n_obs: usize,
22 pub n_entities: usize, pub df_resid: usize, pub sigma: f64,
25 pub inference_type: InferenceType,
26 pub variable_names: Option<Vec<String>>,
27}
28
29impl fmt::Display for PanelResult {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 let stat_label = match self.inference_type {
32 InferenceType::StudentT => "t",
33 InferenceType::Normal => "z",
34 };
35
36 writeln!(f, "\n{:=^78}", " Fixed Effects (Within) Regression ")?;
37 writeln!(
38 f,
39 "{:<20} {:>15} || {:<20} {:>15.4}",
40 "Dep. Variable:", "y", "Within R-sq:", self.r_squared
41 )?;
42 writeln!(
43 f,
44 "{:<20} {:>15} || {:<20} {:>15}",
45 "Estimator:", "Fixed Effects", "No. Entities:", self.n_entities
46 )?;
47 writeln!(
48 f,
49 "{:<20} {:>15} || {:<20} {:>15.4e}",
50 "No. Observations:", self.n_obs, "Sigma:", self.sigma
51 )?;
52
53 writeln!(f, "\n{:-^78}", "")?;
54 writeln!(
55 f,
56 "{:<10} | {:>10} | {:>10} | {:>8} | {:>8}",
57 "Variable",
58 "coef",
59 "std err",
60 stat_label,
61 format!("P>|{}|", stat_label)
62 )?;
63 writeln!(f, "{:-^78}", "")?;
64
65 for i in 0..self.params.len() {
66 let var_name = if let Some(ref names) = self.variable_names {
67 if i < names.len() {
68 names[i].clone()
69 } else {
70 format!("x{}", i)
71 }
72 } else {
73 format!("x{}", i)
74 };
75
76 writeln!(
77 f,
78 "{:<10} | {:>10.4} | {:>10.4} | {:>8.3} | {:>8.3}",
79 var_name, self.params[i], self.std_errors[i], self.t_values[i], self.p_values[i]
80 )?;
81 }
82 writeln!(f, "{:=^78}", "")
83 }
84}
85
86impl PanelResult {
87 pub fn with_inference(mut self, inference_type: InferenceType) -> Result<Self, GreenersError> {
98 use greeners_ols::ols::OlsResult;
100
101 let (p_values, _, _) = OlsResult::compute_inference(
102 &self.t_values,
103 &self.std_errors,
104 &self.params,
105 self.df_resid,
106 &inference_type,
107 )?;
108
109 self.p_values = p_values;
110 self.inference_type = inference_type;
111
112 Ok(self)
113 }
114}
115
116pub struct FixedEffects;
117
118impl FixedEffects {
119 pub fn from_formula<T>(
122 formula: &Formula,
123 data: &DataFrame,
124 entity_ids: &[T],
125 ) -> Result<PanelResult, GreenersError>
126 where
127 T: Eq + Hash + Clone,
128 {
129 Self::from_formula_with_cov(formula, data, entity_ids, CovarianceType::NonRobust)
130 }
131
132 pub fn from_formula_with_cov<T>(
134 formula: &Formula,
135 data: &DataFrame,
136 entity_ids: &[T],
137 cov_type: CovarianceType,
138 ) -> Result<PanelResult, GreenersError>
139 where
140 T: Eq + Hash + Clone,
141 {
142 let (y, x) = data.to_design_matrix(formula)?;
143
144 let var_names: Vec<String> = formula.independents.to_vec();
146
147 Self::fit_with_names(&y, &x, entity_ids, Some(var_names), cov_type)
148 }
149
150 fn within_transform<T>(data: &Array2<f64>, groups: &[T]) -> Result<Array2<f64>, GreenersError>
153 where
154 T: Eq + Hash + Clone,
155 {
156 let n_rows = data.nrows();
157 let n_cols = data.ncols();
158
159 if n_rows != groups.len() {
160 return Err(GreenersError::ShapeMismatch(
161 "Data rows and Group IDs length mismatch".into(),
162 ));
163 }
164
165 let mut group_sums: IndexMap<T, Array1<f64>> = IndexMap::new();
167 let mut group_counts: IndexMap<T, usize> = IndexMap::new();
168
169 for (i, group_id) in groups.iter().enumerate() {
170 let row = data.row(i).to_owned();
171
172 group_sums
173 .entry(group_id.clone())
174 .and_modify(|sum| *sum = &*sum + &row)
175 .or_insert(row);
176
177 *group_counts.entry(group_id.clone()).or_insert(0) += 1;
178 }
179
180 let mut transformed_data = Array2::zeros((n_rows, n_cols));
182
183 for (i, group_id) in groups.iter().enumerate() {
184 let sum = &group_sums[group_id];
185 let count = group_counts[group_id] as f64;
186 let mean = sum / count;
187
188 let original_row = data.row(i);
189 let demeaned_row = &original_row - &mean;
190
191 transformed_data.row_mut(i).assign(&demeaned_row);
192 }
193
194 Ok(transformed_data)
195 }
196
197 pub fn fit<T>(
204 y: &Array1<f64>,
205 x: &Array2<f64>,
206 groups: &[T],
207 ) -> Result<PanelResult, GreenersError>
208 where
209 T: Eq + Hash + Clone,
210 {
211 Self::fit_with_names(y, x, groups, None, CovarianceType::NonRobust)
212 }
213
214 pub fn fit_with_names<T>(
215 y: &Array1<f64>,
216 x: &Array2<f64>,
217 groups: &[T],
218 variable_names: Option<Vec<String>>,
219 cov_type: CovarianceType,
220 ) -> Result<PanelResult, GreenersError>
221 where
222 T: Eq + Hash + Clone,
223 {
224 let n = x.nrows();
225
226 let y_mat = y.view().insert_axis(Axis(1)).to_owned();
228
229 let y_demeaned_mat = Self::within_transform(&y_mat, groups)?;
231 let x_demeaned = Self::within_transform(x, groups)?;
232
233 let y_demeaned = y_demeaned_mat.column(0).to_owned();
235
236 let ols_result = OLS::fit(&y_demeaned, &x_demeaned, cov_type.clone())?;
238
239 let mut unique_groups: IndexMap<T, bool> = IndexMap::new();
241 for g in groups {
242 unique_groups.insert(g.clone(), true);
243 }
244 let n_entities = unique_groups.len();
245
246 let k = x.ncols();
247 let df_resid_correct = n - k - (n_entities - 1); if df_resid_correct == 0 {
250 return Err(GreenersError::ShapeMismatch(
251 "Not enough degrees of freedom for Fixed Effects".into(),
252 ));
253 }
254
255 let residuals = &y_demeaned - &x_demeaned.dot(&ols_result.params);
257 let ssr = residuals.dot(&residuals);
258
259 let sigma2 = ssr / (df_resid_correct as f64);
260 let sigma = sigma2.sqrt();
261
262 let std_errors = if matches!(cov_type, CovarianceType::NonRobust) {
266 let adjustment_factor = (ols_result.df_resid as f64) / (df_resid_correct as f64);
267 let old_vars = ols_result.std_errors.mapv(|se| se.powi(2));
268 (old_vars * adjustment_factor).mapv(f64::sqrt)
269 } else {
270 ols_result.std_errors.clone()
271 };
272
273 let t_values = &ols_result.params / &std_errors;
274
275 let inference_type = ols_result.inference_type.clone();
277
278 use greeners_ols::ols::OlsResult;
280 let (p_values, _, _) = OlsResult::compute_inference(
281 &t_values,
282 &std_errors,
283 &ols_result.params,
284 df_resid_correct,
285 &inference_type,
286 )?;
287
288 Ok(PanelResult {
289 params: ols_result.params,
290 std_errors,
291 t_values,
292 p_values,
293 r_squared: ols_result.r_squared,
294 n_obs: n,
295 n_entities,
296 df_resid: df_resid_correct,
297 sigma,
298 inference_type,
299 variable_names,
300 })
301 }
302}
303
304#[derive(Debug)]
309pub struct RandomEffectsResult {
310 pub params: Array1<f64>,
311 pub std_errors: Array1<f64>,
312 pub t_values: Array1<f64>,
313 pub p_values: Array1<f64>,
314 pub r_squared_overall: f64,
315 pub sigma_u: f64, pub sigma_e: f64, pub theta: f64, pub inference_type: InferenceType,
319 pub variable_names: Option<Vec<String>>,
320}
321
322impl fmt::Display for RandomEffectsResult {
323 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324 let stat_label = match self.inference_type {
325 InferenceType::StudentT => "t",
326 InferenceType::Normal => "z",
327 };
328
329 writeln!(f, "\n{:=^78}", " Random Effects (GLS) - Swamy-Arora ")?;
330 writeln!(
331 f,
332 "{:<20} {:>15.4}",
333 "R-squared (Over):", self.r_squared_overall
334 )?;
335 writeln!(f, "{:<20} {:>15.4}", "Theta:", self.theta)?;
336 writeln!(f, "{:<20} {:>15.4}", "Sigma Alpha (Ind):", self.sigma_e)?;
337 writeln!(f, "{:<20} {:>15.4}", "Sigma U (Idiosync):", self.sigma_u)?;
338
339 writeln!(f, "\n{:-^78}", "")?;
340 writeln!(
341 f,
342 "{:<10} | {:>10} | {:>10} | {:>8} | {:>8}",
343 "Variable",
344 "Coef",
345 "Std Err",
346 stat_label,
347 format!("P>|{}|", stat_label)
348 )?;
349 writeln!(f, "{:-^78}", "")?;
350
351 for i in 0..self.params.len() {
352 let label = self
353 .variable_names
354 .as_ref()
355 .and_then(|v| v.get(i))
356 .cloned()
357 .unwrap_or_else(|| format!("x{}", i));
358 writeln!(
359 f,
360 "{:<10} | {:>10.4} | {:>10.4} | {:>8.3} | {:>8.3}",
361 label, self.params[i], self.std_errors[i], self.t_values[i], self.p_values[i]
362 )?;
363 }
364 writeln!(f, "{:=^78}", "")
365 }
366}
367
368impl RandomEffectsResult {
369 pub fn with_inference(mut self, inference_type: InferenceType) -> Result<Self, GreenersError> {
371 use greeners_ols::ols::OlsResult;
372
373 let df_resid = self.params.len(); let (p_values, _, _) = OlsResult::compute_inference(
378 &self.t_values,
379 &self.std_errors,
380 &self.params,
381 df_resid,
382 &inference_type,
383 )?;
384
385 self.p_values = p_values;
386 self.inference_type = inference_type;
387
388 Ok(self)
389 }
390}
391
392pub struct RandomEffects;
393
394impl RandomEffects {
395 pub fn from_formula(
397 formula: &Formula,
398 data: &DataFrame,
399 entity_ids: &Array1<i64>,
400 ) -> Result<RandomEffectsResult, GreenersError> {
401 let (y, x) = data.to_design_matrix(formula)?;
402 let mut result = Self::fit(&y, &x, entity_ids)?;
403 let mut var_names: Vec<String> = if formula.intercept {
405 let mut v = vec!["const".to_string()];
406 v.extend(formula.independents.iter().cloned());
407 v
408 } else {
409 formula.independents.clone()
410 };
411 var_names.truncate(result.params.len());
412 result.variable_names = Some(var_names);
413 Ok(result)
414 }
415
416 pub fn fit(
417 y: &Array1<f64>,
418 x: &Array2<f64>,
419 entity_ids: &Array1<i64>, ) -> Result<RandomEffectsResult, GreenersError> {
421 let n_obs = y.len();
422 let k = x.ncols();
423
424 if entity_ids.len() != n_obs {
425 return Err(GreenersError::ShapeMismatch(
426 "Entity IDs length mismatch".into(),
427 ));
428 }
429
430 let mut groups: IndexMap<i64, Vec<usize>> = IndexMap::new();
432 for (idx, &id) in entity_ids.iter().enumerate() {
433 groups.entry(id).or_default().push(idx);
435 }
436
437 let n_entities = groups.len();
438 let t_bar = (n_obs as f64) / (n_entities as f64);
440
441 let mut y_within = y.clone();
446 let mut x_within = x.clone();
447
448 let mut y_means = Vec::new();
450 let mut x_means = Vec::new();
451
452 for indices in groups.values() {
453 let t_i = indices.len() as f64;
454
455 let mut y_sum = 0.0;
457 let mut x_sum = Array1::<f64>::zeros(k);
458
459 for &idx in indices {
460 y_sum += y[idx];
461 x_sum = &x_sum + &x.row(idx);
462 }
463 let y_mean = y_sum / t_i;
464 let x_mean = x_sum / t_i;
465
466 y_means.push(y_mean);
467 for val in x_mean.iter() {
469 x_means.push(*val);
470 }
471
472 for &idx in indices {
474 y_within[idx] -= y_mean;
475 let mut row = x_within.row_mut(idx);
476 row -= &x_mean;
477 }
478 }
479
480 let mut keep_indices = Vec::new();
485 for j in 0..k {
486 let col = x_within.column(j);
487 let variance = col.var(0.0); if variance > 1e-12 {
489 keep_indices.push(j);
491 }
492 }
493
494 let x_within_clean = x_within.select(Axis(1), &keep_indices);
496
497 let fe_model = OLS::fit(&y_within, &x_within_clean, CovarianceType::NonRobust)?;
499
500 let residuals_fe = &y_within - &x_within_clean.dot(&fe_model.params);
502 let ssr_within = residuals_fe.mapv(|v| v.powi(2)).sum();
503
504 let k_eff = keep_indices.len();
506 let df_resid_within = (n_obs as f64 - n_entities as f64 - k_eff as f64).max(1.0);
507 let sigma_u_sq = ssr_within / df_resid_within;
508
509 let y_between_arr = Array1::from(y_means);
511 let x_between_arr = Array2::from_shape_vec((n_entities, k), x_means)
513 .map_err(|e| GreenersError::ShapeMismatch(e.to_string()))?;
514
515 let be_model = OLS::fit(&y_between_arr, &x_between_arr, CovarianceType::NonRobust)?;
516
517 let residuals_be = &y_between_arr - &x_between_arr.dot(&be_model.params);
518 let ssr_between = residuals_be.mapv(|v| v.powi(2)).sum();
519
520 let df_resid_between = (n_entities as f64 - k as f64).max(1.0);
522 let sigma_b_sq = ssr_between / df_resid_between;
523
524 let sigma_e_sq = (sigma_b_sq - (sigma_u_sq / t_bar)).max(0.0); let theta = 1.0 - (sigma_u_sq / (sigma_u_sq + t_bar * sigma_e_sq)).sqrt();
530
531 let mut y_gls = y.clone();
534 let mut x_gls = x.clone();
535
536 for indices in groups.values() {
537 let t_i = indices.len() as f64;
538
539 let mut y_sum = 0.0;
541 let mut x_sum = Array1::<f64>::zeros(k);
542 for &idx in indices {
543 y_sum += y[idx];
544 x_sum = &x_sum + &x.row(idx);
545 }
546 let y_mean = y_sum / t_i;
547 let x_mean = x_sum / t_i;
548
549 for &idx in indices {
551 y_gls[idx] -= theta * y_mean;
552 let mut row = x_gls.row_mut(idx);
553 row -= &(&x_mean * theta);
554 }
555 }
556
557 let final_model = OLS::fit(&y_gls, &x_gls, CovarianceType::NonRobust)?;
559
560 let pred_original = x.dot(&final_model.params);
562 let y_mean_total = y.mean().ok_or_else(|| {
563 GreenersError::InvalidOperation("Empty dependent variable".to_string())
564 })?;
565 let sst = (y - y_mean_total).mapv(|v| v.powi(2)).sum();
566 let ssr = (y - &pred_original).mapv(|v| v.powi(2)).sum();
567 let r2_overall = 1.0 - (ssr / sst);
568
569 Ok(RandomEffectsResult {
570 params: final_model.params,
571 std_errors: final_model.std_errors,
572 t_values: final_model.t_values,
573 p_values: final_model.p_values,
574 r_squared_overall: r2_overall,
575 sigma_u: sigma_u_sq.sqrt(),
576 sigma_e: sigma_e_sq.sqrt(),
577 theta,
578 inference_type: final_model.inference_type,
579 variable_names: None,
580 })
581 }
582}
583
584#[derive(Debug)]
585pub struct BetweenResult {
586 pub params: Array1<f64>,
587 pub std_errors: Array1<f64>,
588 pub t_values: Array1<f64>,
589 pub p_values: Array1<f64>,
590 pub r_squared: f64,
591 pub n_entities: usize,
592 pub inference_type: InferenceType,
593}
594
595impl fmt::Display for BetweenResult {
596 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
597 let stat_label = match self.inference_type {
598 InferenceType::StudentT => "t",
599 InferenceType::Normal => "z",
600 };
601
602 writeln!(f, "\n{:=^78}", " Between Estimator (Means) ")?;
603 writeln!(f, "{:<20} {:>15.4}", "R-squared:", self.r_squared)?;
604 writeln!(f, "{:<20} {:>15}", "No. Entities:", self.n_entities)?;
605
606 writeln!(f, "\n{:-^78}", "")?;
607 writeln!(
608 f,
609 "{:<10} | {:>10} | {:>10} | {:>8} | {:>8}",
610 "Variable",
611 "Coef",
612 "Std Err",
613 stat_label,
614 format!("P>|{}|", stat_label)
615 )?;
616 writeln!(f, "{:-^78}", "")?;
617
618 for i in 0..self.params.len() {
619 writeln!(
620 f,
621 "x{:<9} | {:>10.4} | {:>10.4} | {:>8.3} | {:>8.3}",
622 i, self.params[i], self.std_errors[i], self.t_values[i], self.p_values[i]
623 )?;
624 }
625 writeln!(f, "{:=^78}", "")
626 }
627}
628
629impl BetweenResult {
630 pub fn with_inference(mut self, inference_type: InferenceType) -> Result<Self, GreenersError> {
632 use greeners_ols::ols::OlsResult;
633
634 let df_resid = self.n_entities.saturating_sub(self.params.len());
636
637 let (p_values, _, _) = OlsResult::compute_inference(
638 &self.t_values,
639 &self.std_errors,
640 &self.params,
641 df_resid,
642 &inference_type,
643 )?;
644
645 self.p_values = p_values;
646 self.inference_type = inference_type;
647
648 Ok(self)
649 }
650}
651
652pub struct BetweenEstimator;
653
654impl BetweenEstimator {
655 pub fn from_formula(
657 formula: &Formula,
658 data: &DataFrame,
659 entity_ids: &Array1<i64>,
660 ) -> Result<BetweenResult, GreenersError> {
661 let (y, x) = data.to_design_matrix(formula)?;
662 Self::fit(&y, &x, entity_ids)
663 }
664
665 pub fn fit(
668 y: &Array1<f64>,
669 x: &Array2<f64>,
670 entity_ids: &Array1<i64>,
671 ) -> Result<BetweenResult, GreenersError> {
672 let n_obs = y.len();
673 let k = x.ncols();
674
675 if entity_ids.len() != n_obs {
676 return Err(GreenersError::ShapeMismatch(
677 "Entity IDs length mismatch".into(),
678 ));
679 }
680
681 let mut groups: IndexMap<i64, Vec<usize>> = IndexMap::new();
683 for (idx, &id) in entity_ids.iter().enumerate() {
684 groups.entry(id).or_default().push(idx);
685 }
686
687 let n_entities = groups.len();
688
689 let mut y_means = Vec::with_capacity(n_entities);
691 let mut x_means = Vec::with_capacity(n_entities * k);
692
693 for indices in groups.values() {
695 let t_i = indices.len() as f64;
696
697 let mut y_sum = 0.0;
698 let mut x_sum = Array1::<f64>::zeros(k);
699
700 for &idx in indices {
701 y_sum += y[idx];
702 x_sum = &x_sum + &x.row(idx);
703 }
704
705 y_means.push(y_sum / t_i);
706
707 let x_mean = x_sum / t_i;
708 for val in x_mean.iter() {
709 x_means.push(*val);
710 }
711 }
712
713 let y_between = Array1::from(y_means);
714 let x_between = Array2::from_shape_vec((n_entities, k), x_means)
715 .map_err(|e| GreenersError::ShapeMismatch(e.to_string()))?;
716
717 let ols = OLS::fit(&y_between, &x_between, CovarianceType::NonRobust)?;
719
720 Ok(BetweenResult {
721 params: ols.params,
722 std_errors: ols.std_errors,
723 t_values: ols.t_values,
724 p_values: ols.p_values,
725 r_squared: ols.r_squared,
726 n_entities,
727 inference_type: ols.inference_type,
728 })
729 }
730}
731
732#[derive(Debug)]
737pub struct PanelIvResult {
738 pub params: Array1<f64>,
739 pub std_errors: Array1<f64>,
740 pub t_values: Array1<f64>,
741 pub p_values: Array1<f64>,
742 pub r_squared: f64,
743 pub n_obs: usize,
744 pub n_entities: usize,
745 pub df_resid: usize,
746 pub sigma: f64,
747 pub inference_type: InferenceType,
748 pub variable_names: Option<Vec<String>>,
749}
750
751impl fmt::Display for PanelIvResult {
752 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
753 let thick = "═".repeat(70);
754 let thin = "─".repeat(70);
755 writeln!(f, "\n{thick}")?;
756 writeln!(f, " FE-2SLS (xtivreg, fe) — Hausman (1978)")?;
757 writeln!(f, "{thick}")?;
758 writeln!(
759 f,
760 " Obs: {:<8} Entidades: {:<8} df_resid: {}",
761 self.n_obs, self.n_entities, self.df_resid
762 )?;
763 writeln!(
764 f,
765 " R² (within): {:.6} σ: {:.6}",
766 self.r_squared, self.sigma
767 )?;
768 writeln!(f, "{thin}")?;
769 writeln!(
770 f,
771 " {:<18} {:>12} {:>12} {:>8} {:>8}",
772 "Variable", "coef", "SE", "t", "P>|t|"
773 )?;
774 writeln!(f, " {}", "─".repeat(64))?;
775 for i in 0..self.params.len() {
776 let name = self
777 .variable_names
778 .as_ref()
779 .and_then(|v| v.get(i).cloned())
780 .unwrap_or_else(|| format!("x{}", i + 1));
781 writeln!(
782 f,
783 " {:<18} {:>12.4} {:>12.4} {:>8.3} {:>8.4}",
784 name, self.params[i], self.std_errors[i], self.t_values[i], self.p_values[i]
785 )?;
786 }
787 writeln!(f, "{thick}")
788 }
789}
790
791pub struct FE2SLS;
792
793impl FE2SLS {
794 pub fn fit<T>(
802 y: &Array1<f64>,
803 x: &Array2<f64>,
804 z: &Array2<f64>,
805 groups: &[T],
806 variable_names: Option<Vec<String>>,
807 ) -> Result<PanelIvResult, GreenersError>
808 where
809 T: Eq + Hash + Clone,
810 {
811 let n = y.len();
812 let k = x.ncols();
813 let l = z.ncols();
814
815 if x.nrows() != n || z.nrows() != n || groups.len() != n {
816 return Err(GreenersError::ShapeMismatch(
817 "FE2SLS: dimensions of y, x, z and groups differ".into(),
818 ));
819 }
820 if l < k {
821 return Err(GreenersError::ShapeMismatch(format!(
822 "FE2SLS: order condition violated — Z has {l} instruments, X has {k} regressors"
823 )));
824 }
825 if y.iter().any(|v| !v.is_finite())
826 || x.iter().any(|v| !v.is_finite())
827 || z.iter().any(|v| !v.is_finite())
828 {
829 return Err(GreenersError::InvalidOperation(
830 "FE2SLS: data contain NaN or Inf".into(),
831 ));
832 }
833
834 let y_mat = y.view().insert_axis(Axis(1)).to_owned();
836 let y_dm = FixedEffects::within_transform(&y_mat, groups)?;
837 let x_dm = FixedEffects::within_transform(x, groups)?;
838 let z_dm = FixedEffects::within_transform(z, groups)?;
839
840 let y_d: Array1<f64> = y_dm.column(0).to_owned();
841
842 let zt = z_dm.t();
844 let zt_z = zt.dot(&z_dm);
845 let zt_z_inv = zt_z.inv()?;
846 let zt_x = zt.dot(&x_dm);
847 let x_hat = z_dm.dot(&zt_z_inv.dot(&zt_x));
848
849 let xht = x_hat.t();
851 let xht_xd = xht.dot(&x_dm);
852 let xht_xd_inv = xht_xd.inv()?;
853 let beta = xht_xd_inv.dot(&xht.dot(&y_d));
854
855 let fitted = x_dm.dot(&beta);
857 let resid = &y_d - &fitted;
858 let ssr = resid.dot(&resid);
859
860 let n_entities = {
861 let mut seen = IndexMap::new();
862 for g in groups {
863 seen.insert(g.clone(), ());
864 }
865 seen.len()
866 };
867 let df_resid = n.saturating_sub(k).saturating_sub(n_entities - 1);
869 if df_resid == 0 {
870 return Err(GreenersError::ShapeMismatch(
871 "FE2SLS: insufficient degrees of freedom".into(),
872 ));
873 }
874
875 let sigma2 = ssr / df_resid as f64;
876 let sigma = sigma2.sqrt();
877
878 let cov_mat = &xht_xd_inv * sigma2;
880 let std_errors: Array1<f64> = (0..k)
881 .map(|i| cov_mat[[i, i]].max(0.0).sqrt())
882 .collect::<Vec<_>>()
883 .into();
884
885 let t_values = &beta / &std_errors;
886
887 use statrs::distribution::{ContinuousCDF, StudentsT};
889 let t_dist = StudentsT::new(0.0, 1.0, df_resid as f64)
890 .map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
891 let p_values: Array1<f64> = t_values
892 .iter()
893 .map(|&t| 2.0 * (1.0 - t_dist.cdf(t.abs())))
894 .collect::<Vec<_>>()
895 .into();
896
897 let r_squared = {
899 let ymean = y_d.mean().unwrap_or(0.0);
900 let ss_tot: f64 = y_d.iter().map(|&v| (v - ymean).powi(2)).sum();
901 let ss_res: f64 = ssr;
902 if ss_tot > 1e-15 {
903 1.0 - ss_res / ss_tot
904 } else {
905 0.0
906 }
907 };
908
909 Ok(PanelIvResult {
910 params: beta,
911 std_errors,
912 t_values,
913 p_values,
914 r_squared,
915 n_obs: n,
916 n_entities,
917 df_resid,
918 sigma,
919 inference_type: InferenceType::StudentT,
920 variable_names,
921 })
922 }
923}
924
925type BalancedPanelResult = (Vec<i64>, Vec<Array1<f64>>, Vec<Array2<f64>>, usize);
930
931fn extract_balanced_panels(
935 y: &Array1<f64>,
936 x: &Array2<f64>,
937 entity_ids: &[i64],
938 time_ids: &[i64],
939) -> Result<BalancedPanelResult, GreenersError> {
940 let n = y.len();
941 if x.nrows() != n || entity_ids.len() != n || time_ids.len() != n {
942 return Err(GreenersError::ShapeMismatch(
943 "dimensions of y, x, entity_ids, time_ids differ".into(),
944 ));
945 }
946
947 let mut order: Vec<usize> = (0..n).collect();
949 order.sort_by_key(|&i| (entity_ids[i], time_ids[i]));
950
951 let mut entities: Vec<i64> = Vec::new();
953 for &i in &order {
954 let e = entity_ids[i];
955 if entities.last() != Some(&e) {
956 entities.push(e);
957 }
958 }
959
960 let mut counts: IndexMap<i64, usize> = IndexMap::new();
962 for &i in &order {
963 *counts.entry(entity_ids[i]).or_insert(0) += 1;
964 }
965 let t_vec: Vec<usize> = entities.iter().map(|e| counts[e]).collect();
966 let t0 = t_vec[0];
967 if t_vec.iter().any(|&t| t != t0) {
968 return Err(GreenersError::InvalidOperation(
969 "unbalanced panel: number of periods differs between entities".into(),
970 ));
971 }
972
973 let k = x.ncols();
974 let mut y_panels: Vec<Array1<f64>> = Vec::with_capacity(entities.len());
975 let mut x_panels: Vec<Array2<f64>> = Vec::with_capacity(entities.len());
976
977 for &eid in &entities {
978 let rows: Vec<usize> = order
979 .iter()
980 .filter(|&&i| entity_ids[i] == eid)
981 .copied()
982 .collect();
983 let yi: Array1<f64> = rows.iter().map(|&i| y[i]).collect::<Vec<_>>().into();
984 let mut xi = Array2::<f64>::zeros((t0, k));
985 for (r, &src) in rows.iter().enumerate() {
986 xi.row_mut(r).assign(&x.row(src));
987 }
988 y_panels.push(yi);
989 x_panels.push(xi);
990 }
991
992 Ok((entities, y_panels, x_panels, t0))
993}
994
995fn t_pvalues(t_vals: &Array1<f64>, df: usize) -> Result<Array1<f64>, GreenersError> {
996 use statrs::distribution::{ContinuousCDF, StudentsT};
997 let dist = StudentsT::new(0.0, 1.0, df as f64)
998 .map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
999 Ok(t_vals.mapv(|t| 2.0 * (1.0 - dist.cdf(t.abs()))))
1000}
1001
1002#[derive(Debug)]
1008pub struct PcseResult {
1009 pub params: Array1<f64>,
1010 pub std_errors: Array1<f64>,
1011 pub t_values: Array1<f64>,
1012 pub p_values: Array1<f64>,
1013 pub r_squared: f64,
1014 pub n_obs: usize,
1015 pub n_entities: usize,
1016 pub t_periods: usize,
1017 pub df_resid: usize,
1018 pub sigma: f64,
1019 pub variable_names: Option<Vec<String>>,
1020}
1021
1022impl fmt::Display for PcseResult {
1023 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1024 let thick = "═".repeat(70);
1025 let thin = "─".repeat(70);
1026 writeln!(f, "\n{thick}")?;
1027 writeln!(
1028 f,
1029 " PCSE — Panel-Corrected Standard Errors (Beck & Katz 1995)"
1030 )?;
1031 writeln!(f, "{thick}")?;
1032 writeln!(
1033 f,
1034 " Obs: {:<8} Entidades: {:<6} Periods: {:<6} df_resid: {}",
1035 self.n_obs, self.n_entities, self.t_periods, self.df_resid
1036 )?;
1037 writeln!(f, " R²: {:.6} σ (OLS): {:.6}", self.r_squared, self.sigma)?;
1038 writeln!(f, "{thin}")?;
1039 writeln!(
1040 f,
1041 " {:<18} {:>12} {:>12} {:>8} {:>8}",
1042 "Variable", "coef", "PCSE", "t", "P>|t|"
1043 )?;
1044 writeln!(f, " {}", "─".repeat(64))?;
1045 for i in 0..self.params.len() {
1046 let name = self
1047 .variable_names
1048 .as_ref()
1049 .and_then(|v| v.get(i).cloned())
1050 .unwrap_or_else(|| format!("x{}", i + 1));
1051 writeln!(
1052 f,
1053 " {:<18} {:>12.4} {:>12.4} {:>8.3} {:>8.4}",
1054 name, self.params[i], self.std_errors[i], self.t_values[i], self.p_values[i]
1055 )?;
1056 }
1057 writeln!(f, "{thick}")
1058 }
1059}
1060
1061pub struct PCSE;
1062
1063impl PCSE {
1064 pub fn fit(
1065 y: &Array1<f64>,
1066 x: &Array2<f64>,
1067 entity_ids: &[i64],
1068 time_ids: &[i64],
1069 variable_names: Option<Vec<String>>,
1070 ) -> Result<PcseResult, GreenersError> {
1071 if y.iter().any(|v| !v.is_finite()) || x.iter().any(|v| !v.is_finite()) {
1072 return Err(GreenersError::InvalidOperation(
1073 "PCSE: data contain NaN or Inf".into(),
1074 ));
1075 }
1076
1077 let (_, y_panels, x_panels, big_t) = extract_balanced_panels(y, x, entity_ids, time_ids)?;
1078
1079 let n_entities = y_panels.len();
1080 let n_obs = n_entities * big_t;
1081 let k = x.ncols();
1082 let df_resid = n_obs.saturating_sub(k);
1083
1084 let xtx: Array2<f64> = x_panels
1086 .iter()
1087 .fold(Array2::zeros((k, k)), |acc, xi| acc + xi.t().dot(xi));
1088 let xty: Array1<f64> = x_panels
1089 .iter()
1090 .zip(y_panels.iter())
1091 .fold(Array1::zeros(k), |acc, (xi, yi)| acc + xi.t().dot(yi));
1092 let xtx_inv = xtx.inv()?;
1093 let beta = xtx_inv.dot(&xty);
1094
1095 let resid_panels: Vec<Array1<f64>> = y_panels
1097 .iter()
1098 .zip(x_panels.iter())
1099 .map(|(yi, xi)| yi - &xi.dot(&beta))
1100 .collect();
1101 let ssr: f64 = resid_panels.iter().map(|e| e.dot(e)).sum();
1102 let sigma = (ssr / df_resid as f64).sqrt();
1103
1104 let n = n_entities;
1106 let mut sigma_hat = Array2::<f64>::zeros((n, n));
1107 for i in 0..n {
1108 for j in i..n {
1109 let s = resid_panels[i].dot(&resid_panels[j]) / big_t as f64;
1110 sigma_hat[[i, j]] = s;
1111 sigma_hat[[j, i]] = s;
1112 }
1113 }
1114
1115 let mut meat = Array2::<f64>::zeros((k, k));
1117 for i in 0..n {
1118 for j in 0..n {
1119 meat = meat + x_panels[i].t().dot(&x_panels[j]) * sigma_hat[[i, j]];
1120 }
1121 }
1122
1123 let v = xtx_inv.dot(&meat).dot(&xtx_inv);
1125 let std_errors: Array1<f64> = (0..k)
1126 .map(|i| v[[i, i]].max(0.0).sqrt())
1127 .collect::<Vec<_>>()
1128 .into();
1129 let t_values = &beta / &std_errors;
1130 let p_values = t_pvalues(&t_values, df_resid)?;
1131
1132 let ymean = y.mean().unwrap_or(0.0);
1134 let ss_tot: f64 = y.iter().map(|&v| (v - ymean).powi(2)).sum();
1135 let r_squared = if ss_tot > 1e-15 {
1136 1.0 - ssr / ss_tot
1137 } else {
1138 0.0
1139 };
1140
1141 Ok(PcseResult {
1142 params: beta,
1143 std_errors,
1144 t_values,
1145 p_values,
1146 r_squared,
1147 n_obs,
1148 n_entities,
1149 t_periods: big_t,
1150 df_resid,
1151 sigma,
1152 variable_names,
1153 })
1154 }
1155}
1156
1157#[derive(Debug, Clone, PartialEq)]
1164pub enum GlsPanels {
1165 Hetero,
1166 Correlated,
1167}
1168
1169#[derive(Debug)]
1170pub struct PanelGlsResult {
1171 pub params: Array1<f64>,
1172 pub std_errors: Array1<f64>,
1173 pub t_values: Array1<f64>,
1174 pub p_values: Array1<f64>,
1175 pub r_squared: f64,
1176 pub n_obs: usize,
1177 pub n_entities: usize,
1178 pub t_periods: usize,
1179 pub df_resid: usize,
1180 pub sigma: f64,
1181 pub panels: GlsPanels,
1182 pub variable_names: Option<Vec<String>>,
1183}
1184
1185impl fmt::Display for PanelGlsResult {
1186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1187 let method = match self.panels {
1188 GlsPanels::Hetero => "heteroscedastic (diagonal Σ)",
1189 GlsPanels::Correlated => "correlated (Parks, Σ completa)",
1190 };
1191 let thick = "═".repeat(70);
1192 let thin = "─".repeat(70);
1193 writeln!(f, "\n{thick}")?;
1194 writeln!(f, " Panel GLS — panels({})", method)?;
1195 writeln!(f, "{thick}")?;
1196 writeln!(
1197 f,
1198 " Obs: {:<8} Entidades: {:<6} Periods: {:<6} df_resid: {}",
1199 self.n_obs, self.n_entities, self.t_periods, self.df_resid
1200 )?;
1201 writeln!(f, " R²: {:.6} σ (GLS): {:.6}", self.r_squared, self.sigma)?;
1202 writeln!(f, "{thin}")?;
1203 writeln!(
1204 f,
1205 " {:<18} {:>12} {:>12} {:>8} {:>8}",
1206 "Variable", "coef", "SE", "z", "P>|z|"
1207 )?;
1208 writeln!(f, " {}", "─".repeat(64))?;
1209 for i in 0..self.params.len() {
1210 let name = self
1211 .variable_names
1212 .as_ref()
1213 .and_then(|v| v.get(i).cloned())
1214 .unwrap_or_else(|| format!("x{}", i + 1));
1215 writeln!(
1216 f,
1217 " {:<18} {:>12.4} {:>12.4} {:>8.3} {:>8.4}",
1218 name, self.params[i], self.std_errors[i], self.t_values[i], self.p_values[i]
1219 )?;
1220 }
1221 writeln!(f, "{thick}")
1222 }
1223}
1224
1225pub struct PanelGLS;
1226
1227impl PanelGLS {
1228 pub fn fit(
1229 y: &Array1<f64>,
1230 x: &Array2<f64>,
1231 entity_ids: &[i64],
1232 time_ids: &[i64],
1233 panels: GlsPanels,
1234 variable_names: Option<Vec<String>>,
1235 ) -> Result<PanelGlsResult, GreenersError> {
1236 if y.iter().any(|v| !v.is_finite()) || x.iter().any(|v| !v.is_finite()) {
1237 return Err(GreenersError::InvalidOperation(
1238 "PanelGLS: data contain NaN or Inf".into(),
1239 ));
1240 }
1241
1242 let (_, y_panels, x_panels, big_t) = extract_balanced_panels(y, x, entity_ids, time_ids)?;
1243
1244 let n_entities = y_panels.len();
1245 let n_obs = n_entities * big_t;
1246 let k = x.ncols();
1247 let df_resid = n_obs.saturating_sub(k);
1248
1249 let xtx0: Array2<f64> = x_panels
1251 .iter()
1252 .fold(Array2::zeros((k, k)), |acc, xi| acc + xi.t().dot(xi));
1253 let xty0: Array1<f64> = x_panels
1254 .iter()
1255 .zip(y_panels.iter())
1256 .fold(Array1::zeros(k), |acc, (xi, yi)| acc + xi.t().dot(yi));
1257 let beta0 = xtx0.inv()?.dot(&xty0);
1258 let resid0: Vec<Array1<f64>> = y_panels
1259 .iter()
1260 .zip(x_panels.iter())
1261 .map(|(yi, xi)| yi - &xi.dot(&beta0))
1262 .collect();
1263
1264 let n = n_entities;
1266 let (xtox, xtoy) = match panels {
1267 GlsPanels::Hetero => {
1268 let mut xtox = Array2::<f64>::zeros((k, k));
1270 let mut xtoy = Array1::<f64>::zeros(k);
1271 for i in 0..n {
1272 let sigma2_i = resid0[i].dot(&resid0[i]) / big_t as f64;
1273 if sigma2_i < 1e-15 {
1274 return Err(GreenersError::InvalidOperation(format!(
1275 "PanelGLS: σ²_i ≈ 0 for entity {i} — perfectly fitted residuals?"
1276 )));
1277 }
1278 let w = 1.0 / sigma2_i;
1279 xtox = xtox + x_panels[i].t().dot(&x_panels[i]) * w;
1280 xtoy = xtoy + x_panels[i].t().dot(&y_panels[i]) * w;
1281 }
1282 (xtox, xtoy)
1283 }
1284 GlsPanels::Correlated => {
1285 let mut sigma_hat = Array2::<f64>::zeros((n, n));
1287 for i in 0..n {
1288 for j in i..n {
1289 let s = resid0[i].dot(&resid0[j]) / big_t as f64;
1290 sigma_hat[[i, j]] = s;
1291 sigma_hat[[j, i]] = s;
1292 }
1293 }
1294 let sigma_inv = sigma_hat.inv()?;
1295
1296 let mut xtox = Array2::<f64>::zeros((k, k));
1297 let mut xtoy = Array1::<f64>::zeros(k);
1298 for i in 0..n {
1299 for j in 0..n {
1300 let w = sigma_inv[[i, j]];
1301 xtox = xtox + x_panels[i].t().dot(&x_panels[j]) * w;
1302 xtoy = xtoy + x_panels[i].t().dot(&y_panels[j]) * w;
1303 }
1304 }
1305 (xtox, xtoy)
1306 }
1307 };
1308
1309 let xtox_inv = xtox.inv()?;
1311 let beta = xtox_inv.dot(&xtoy);
1312
1313 let resid_gls: Vec<Array1<f64>> = y_panels
1315 .iter()
1316 .zip(x_panels.iter())
1317 .map(|(yi, xi)| yi - &xi.dot(&beta))
1318 .collect();
1319 let ssr_gls: f64 = resid_gls.iter().map(|e| e.dot(e)).sum();
1320 let sigma = (ssr_gls / df_resid as f64).sqrt();
1321
1322 let std_errors: Array1<f64> = (0..k)
1324 .map(|i| xtox_inv[[i, i]].max(0.0).sqrt())
1325 .collect::<Vec<_>>()
1326 .into();
1327 let t_values = &beta / &std_errors;
1328 use statrs::distribution::{ContinuousCDF, Normal};
1330 let norm =
1331 Normal::new(0.0, 1.0).map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
1332 let p_values: Array1<f64> = t_values.mapv(|z| 2.0 * (1.0 - norm.cdf(z.abs())));
1333
1334 let ymean = y.mean().unwrap_or(0.0);
1336 let ss_tot: f64 = y.iter().map(|&v| (v - ymean).powi(2)).sum();
1337 let r_squared = if ss_tot > 1e-15 {
1338 1.0 - ssr_gls / ss_tot
1339 } else {
1340 0.0
1341 };
1342
1343 Ok(PanelGlsResult {
1344 params: beta,
1345 std_errors,
1346 t_values,
1347 p_values,
1348 r_squared,
1349 n_obs,
1350 n_entities,
1351 t_periods: big_t,
1352 df_resid,
1353 sigma,
1354 panels,
1355 variable_names,
1356 })
1357 }
1358}