Skip to main content

greeners_panel/
panel.rs

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// ===========================================================================
10// FIXED EFFECTS (WITHIN ESTIMATOR)
11// ===========================================================================
12
13/// Struct to hold Fixed Effects estimation results.
14#[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, // "Within" R-squared
21    pub n_obs: usize,
22    pub n_entities: usize, // Number of unique groups (N)
23    pub df_resid: usize,   // Corrected degrees of freedom
24    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    /// Change inference type and recompute p-values
88    ///
89    /// Allows switching between Student's t-distribution and Normal distribution
90    /// for hypothesis testing after model fitting.
91    ///
92    /// # Arguments
93    /// * `inference_type` - New distribution type
94    ///
95    /// # Returns
96    /// Modified PanelResult with updated p-values
97    pub fn with_inference(mut self, inference_type: InferenceType) -> Result<Self, GreenersError> {
98        // Reuse OLS compute_inference helper
99        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    /// Estimates Fixed Effects model using a formula and DataFrame.
120    /// Requires entity_ids to be passed separately.
121    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    /// Estimates Fixed Effects model with a specified covariance type.
133    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        // Build variable names from formula (no intercept in FE)
145        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    /// Performs the "Within Transformation" (Demeaning) on a matrix/vector.
151    /// x_dem = x_it - mean(x_i)
152    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        // 1. Calculate sums and counts per group
166        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        // 2. Subtract group means from original data
181        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    /// Fits the Fixed Effects model using Within Estimation.
198    ///
199    /// # Arguments
200    /// * `y` - Dependent variable.
201    /// * `x` - Regressors (DO NOT includes a constant/intercept column!).
202    /// * `groups` - Vector of Entity IDs (Integers, Strings, etc.) corresponding to rows.
203    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        // 1. Convert y to Array2 for the generic transform function
227        let y_mat = y.view().insert_axis(Axis(1)).to_owned();
228
229        // 2. Apply Within Transformation
230        let y_demeaned_mat = Self::within_transform(&y_mat, groups)?;
231        let x_demeaned = Self::within_transform(x, groups)?;
232
233        // Flatten y back to Array1
234        let y_demeaned = y_demeaned_mat.column(0).to_owned();
235
236        // 3. Run OLS on demeaned data with requested covariance type
237        let ols_result = OLS::fit(&y_demeaned, &x_demeaned, cov_type.clone())?;
238
239        // 4. Degrees of Freedom Correction
240        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); // FE correction
248
249        if df_resid_correct == 0 {
250            return Err(GreenersError::ShapeMismatch(
251                "Not enough degrees of freedom for Fixed Effects".into(),
252            ));
253        }
254
255        // Recalculate Sigma and Standard Errors with correct DF
256        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        // For NonRobust covariance, scale SEs by the FE degrees-of-freedom
263        // correction. For robust/clustered/Newey-West, use the SEs already
264        // produced by OLS on the demeaned data and only recompute inference.
265        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        // Extract inference type from OLS result
276        let inference_type = ols_result.inference_type.clone();
277
278        // Recalculate p-values with corrected df_resid
279        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// ===========================================================================
305// RANDOM EFFECTS (SWAMY-ARORA GLS)
306// ===========================================================================
307
308#[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, //Standard deviation of idiosyncratic error
316    pub sigma_e: f64, //Standard deviation of the individual effect
317    pub theta: f64,   //Weight of transformation GLS
318    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    /// Change inference type and recompute p-values
370    pub fn with_inference(mut self, inference_type: InferenceType) -> Result<Self, GreenersError> {
371        use greeners_ols::ols::OlsResult;
372
373        // Random Effects doesn't have df_resid field, so we compute it
374        // df = n - k where n is observations and k is parameters
375        let df_resid = self.params.len(); // This is approximate; ideally should be stored
376
377        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    /// Estimates Random Effects model using a formula and DataFrame.
396    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        // to_design_matrix coloca intercepto primeiro quando formula.intercept == true
404        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>, //IDs of individuals/enterprises
420    ) -> 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        //1. Map Indexes per Entity
431        let mut groups: IndexMap<i64, Vec<usize>> = IndexMap::new();
432        for (idx, &id) in entity_ids.iter().enumerate() {
433            // CORREÇÃO: or_insert em vez de or_insert_vec
434            groups.entry(id).or_default().push(idx);
435        }
436
437        let n_entities = groups.len();
438        //Assuming balanced panel for simplified calculation of Theta (average T)
439        let t_bar = (n_obs as f64) / (n_entities as f64);
440
441        //Step 2: Estimate Variances (Swamy-Arora) ---
442
443        // A. Variância Within (Fixed Effects) -> Sigma_u
444        // Transformação Within manual: (x_it - x_i_bar)
445        let mut y_within = y.clone();
446        let mut x_within = x.clone();
447
448        // B. Variância Between (Médias) -> Para Sigma_e
449        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            //Calculate group averages
456            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            // Flatten x_mean para o vetor de between
468            for val in x_mean.iter() {
469                x_means.push(*val);
470            }
471
472            //Subtract Medium (Within Transformation)
473            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        //--- SINGULARITY CORRECTION (CRUCIAL) ---
481        //By subtracting the average, constant columns (as intercept) turned zero.
482        //This breaks the matrix inversion of OLS. We need to filter these columns.
483        // apenas para o passo intermediário de calcular Sigma_u.
484        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); //Population variance of the column
488            if variance > 1e-12 {
489                //If there's variation, we keep
490                keep_indices.push(j);
491            }
492        }
493
494        // Selecionar apenas colunas que variam
495        let x_within_clean = x_within.select(Axis(1), &keep_indices);
496
497        // Rodar OLS nos dados filtrados (sem intercepto) para pegar Sigma_u
498        let fe_model = OLS::fit(&y_within, &x_within_clean, CovarianceType::NonRobust)?;
499
500        //Calculate residuals using filtered data and betas obtained
501        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        //Corrected degrees of freedom (using effective columns)
505        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        //Rotate OLS in Between data (Mediums)
510        let y_between_arr = Array1::from(y_means);
511        // CORREÇÃO: Tratamento de erro no from_shape_vec
512        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        // Variância composta do between = sigma_u^2 / T + sigma_e^2
521        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        // Recuperar Sigma_e (Effect Individual)
525        // sigma_e^2 = sigma_b^2 - (sigma_u^2 / T)
526        let sigma_e_sq = (sigma_b_sq - (sigma_u_sq / t_bar)).max(0.0); //max(0) to avoid negative variance
527
528        //--- STEP 3: Transformation GLS (Theta) ---
529        let theta = 1.0 - (sigma_u_sq / (sigma_u_sq + t_bar * sigma_e_sq)).sqrt();
530
531        // --- PASSO 4: Transformar Dados Finais ---
532        // y* = y_it - theta * y_i_bar
533        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            //Recalculate average (fast)
540            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            //Apply Quasi-Difference
550            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        // --- PASSO 5: OLS Final ---
558        let final_model = OLS::fit(&y_gls, &x_gls, CovarianceType::NonRobust)?;
559
560        //R2 Overall (Correlation between predicted Y and original real Y)
561        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    /// Change inference type and recompute p-values
631    pub fn with_inference(mut self, inference_type: InferenceType) -> Result<Self, GreenersError> {
632        use greeners_ols::ols::OlsResult;
633
634        // Between estimator uses n_entities as sample size
635        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    /// Estimates Between model using a formula and DataFrame.
656    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    /// Estimates the regression in the temporal means of each individual.
666    /// y_bar_i = alpha + beta * x_bar_i + (alpha_i + u_bar_i)
667    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        //1. Group by Entity
682        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        //2. Calculate Averages (Collapse)
690        let mut y_means = Vec::with_capacity(n_entities);
691        let mut x_means = Vec::with_capacity(n_entities * k);
692
693        //Iterate over groups to create reduced dataset (N x K)
694        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        // 3. Rodar OLS no dataset colapsado
718        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// ===========================================================================
733// FE-2SLS (xtivreg, fe) — Hausman (1978)
734// ===========================================================================
735
736#[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    /// Estimates FE-2SLS: `feiv(y ~ x1+x2, ~ x1+z1+z2, df, id=col)`
795    ///
796    /// * `y` — dependent variable (n)
797    /// * `x` — structural regressors without constant (n × k); endogenous column included
798    /// * `z` — full instrument matrix without constant (n × l), l ≥ k;
799    ///   must include exogenous regressors + excluded instruments
800    /// * `groups` — entity IDs for within transformation
801    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        //── 1. Transformation within (demean) ──
835        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        // ── 2. 2SLS: primeira etapa X̂ = P_Z X̃ ──
843        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        // ── 3. Segunda etapa: β = (X̂'X̃)⁻¹ X̂'ỹ ──
850        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        //── 4. Residues and degrees of freedom ──
856        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        //FE consumes N-1 degrees of freedom (individual effects demeaned)
868        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        // ── 5. V = σ² (X̂'X̃)⁻¹ ──
879        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        // ── 6. p-values (t com df_resid) ──
888        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        // ── 7. R² within: corr²(ỹ, X̃β) ──
898        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
925// ===========================================================================
926//Shared helpers — balanced panel extraction
927// ===========================================================================
928
929type BalancedPanelResult = (Vec<i64>, Vec<Array1<f64>>, Vec<Array2<f64>>, usize);
930
931/// Extract submatrixes per entity from long data, ordering by
932/// (entity_id, time_id). Exige painel balanceado (T igual para todas as entidades).
933/// Retorna (entidades_ordenadas, y_panels, x_panels, T).
934fn 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    //Sort indexes by (entity, time)
948    let mut order: Vec<usize> = (0..n).collect();
949    order.sort_by_key(|&i| (entity_ids[i], time_ids[i]));
950
951    //Collect unique entities in order
952    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    //T account per entity and check balance
961    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// ===========================================================================
1003// PCSE — Panel-Corrected Standard Errors (Beck & Katz 1995)
1004// Stata: xtpcse y x1 x2, id(firm) t(year)
1005// ===========================================================================
1006
1007#[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        // ── OLS β̂ = (X'X)⁻¹ X'y ──
1085        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        // ── Resíduos e σ ──
1096        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        // ── Σ̂_ij = e_i'e_j / T ──
1105        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        // ── Meat = Σ_i Σ_j σ̂_ij X_i'X_j ──
1116        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        // ── V_PCSE = (X'X)⁻¹ Meat (X'X)⁻¹ ──
1124        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        // ── R² ──
1133        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// ===========================================================================
1158// Panel GLS — Parks (1967) / Stata xtgls
1159// panels=hetero : σ²_i por entidade (diagonal Σ)
1160// panels=corr   : Σ completa entre entidades (Parks clássico)
1161// ===========================================================================
1162
1163#[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        //── Step 1: OLS for initial residuals ──
1250        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        // ── Passo 2: estimar Σ̂ ──
1265        let n = n_entities;
1266        let (xtox, xtoy) = match panels {
1267            GlsPanels::Hetero => {
1268                // Diagonal: σ̂²_i = e_i'e_i / T
1269                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                // Σ̂ completa: σ̂_ij = e_i'e_j / T, depois inverte
1286                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        // ── Passo 3: β̂_GLS = (X'Ω⁻¹X)⁻¹ X'Ω⁻¹y ──
1310        let xtox_inv = xtox.inv()?;
1311        let beta = xtox_inv.dot(&xtoy);
1312
1313        // ── Resíduos GLS e σ ──
1314        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        // ── V_GLS = (X'Ω⁻¹X)⁻¹  (SE assintótica, usa Normal) ──
1323        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        //Parks uses normal distribution (z), not t
1329        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        // ── R² ──
1335        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}