use crate::linalg::LinalgInverse as _;
use crate::panel::{PanelResult, RandomEffectsResult};
use crate::GreenersError;
use ndarray::{Array1, Array2};
use statrs::distribution::{ChiSquared, ContinuousCDF, FisherSnedecor};
use std::fmt;
#[derive(Debug)]
pub struct RobustHausmanResult {
pub chi2: f64,
pub df: usize,
pub p_value: f64,
pub beta_diff: Array1<f64>,
pub cov_diff: Array2<f64>,
pub reject_h0: bool,
pub recommendation: String,
pub n_coef: usize,
pub method: String,
}
impl fmt::Display for RobustHausmanResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n{:=^78}", " Robust Hausman Test ")?;
writeln!(f, "Cameron-Trivedi (2005); Wooldridge (2010)")?;
writeln!(f, "H0: Random Effects is consistent")?;
writeln!(f, "H1: Random Effects is inconsistent (use FE)")?;
writeln!(f, "{:<20} {:>12}", "Method:", self.method)?;
writeln!(f, "{:<20} {:>12}", "Coefficients:", self.n_coef)?;
writeln!(f, "{:<20} {:>12}", "df:", self.df)?;
writeln!(f, "{:<20} {:>12.4}", "Chi2:", self.chi2)?;
writeln!(f, "{:<20} {:>12.4}", "P-value:", self.p_value)?;
writeln!(f, "\n{:-^78}", "")?;
writeln!(f, " Coefficient differences (FE - RE):")?;
writeln!(f, " {:<10} {:>12}", "Coef", "FE - RE")?;
writeln!(f, "{:-^78}", "")?;
for i in 0..self.n_coef {
writeln!(
f,
" {:<10} {:>12.6}",
format!("b{}", i + 1),
self.beta_diff[i]
)?;
}
writeln!(f, "\n Result: {}", self.recommendation)?;
write!(f, "{:=^78}", "")
}
}
#[derive(Debug)]
pub struct RobustFTestResult {
pub wald_chi2: f64,
pub f_stat: f64,
pub df_num: usize,
pub df_denom: usize,
pub p_value: f64,
pub p_value_chi2: f64,
pub reject_h0: bool,
pub h0: String,
pub tested_coefs: Vec<String>,
pub n_restrictions: usize,
}
impl fmt::Display for RobustFTestResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n{:=^78}", " Robust F-Test (Panel) ")?;
writeln!(f, "Wooldridge (2010)")?;
writeln!(f, "H0: {}", self.h0)?;
writeln!(f, "{:<20} {:>12}", "Restrictions:", self.n_restrictions)?;
writeln!(f, "{:<20} {:>12}", "df (num):", self.df_num)?;
writeln!(f, "{:<20} {:>12}", "df (denom):", self.df_denom)?;
writeln!(f, "{:<20} {:>12.4}", "Wald Chi2:", self.wald_chi2)?;
writeln!(f, "{:<20} {:>12.4}", "F-stat:", self.f_stat)?;
writeln!(f, "{:<20} {:>12.4}", "P-value (F):", self.p_value)?;
writeln!(f, "{:<20} {:>12.4}", "P-value (Chi2):", self.p_value_chi2)?;
writeln!(f, "\n{:-^78}", "")?;
writeln!(f, " Tested coefficients:")?;
for name in &self.tested_coefs {
writeln!(f, " - {}", name)?;
}
let verdict = if self.reject_h0 {
"Reject H0: coefficients are jointly significant."
} else {
"Fail to reject H0: coefficients are not jointly significant."
};
writeln!(f, "\n Result: {}", verdict)?;
write!(f, "{:=^78}", "")
}
}
pub struct RobustHausman;
impl RobustHausman {
pub fn compare(
fe: &PanelResult,
re: &RandomEffectsResult,
fe_vcov: &Array2<f64>,
re_vcov: &Array2<f64>,
) -> Result<RobustHausmanResult, GreenersError> {
Self::compare_arrays(
&fe.params,
&re.params,
fe_vcov,
re_vcov,
fe.variable_names.as_deref(),
)
}
pub fn compare_arrays(
fe_beta: &Array1<f64>,
re_beta: &Array1<f64>,
fe_vcov: &Array2<f64>,
re_vcov: &Array2<f64>,
_var_names: Option<&[String]>,
) -> Result<RobustHausmanResult, GreenersError> {
let k = fe_beta.len();
if re_beta.len() != k {
return Err(GreenersError::ShapeMismatch(
"RobustHausman: FE and RE must have same number of params".into(),
));
}
if fe_vcov.nrows() != k || fe_vcov.ncols() != k {
return Err(GreenersError::ShapeMismatch(
"RobustHausman: fe_vcov must be k x k".into(),
));
}
if re_vcov.nrows() != k || re_vcov.ncols() != k {
return Err(GreenersError::ShapeMismatch(
"RobustHausman: re_vcov must be k x k".into(),
));
}
let beta_diff = fe_beta - re_beta;
let cov_diff = fe_vcov - re_vcov;
let cov_diff_inv = cov_diff.inv()?;
let wald = beta_diff.dot(&cov_diff_inv.dot(&beta_diff));
let chi2 = wald.max(0.0);
let dist = ChiSquared::new(k as f64)
.map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
let p_value = 1.0 - dist.cdf(chi2);
let reject_h0 = p_value < 0.05;
let recommendation = if reject_h0 {
"Reject H0. Use FIXED EFFECTS (RE is inconsistent)."
} else {
"Fail to reject H0. Use RANDOM EFFECTS (it is efficient)."
};
Ok(RobustHausmanResult {
chi2,
df: k,
p_value,
beta_diff,
cov_diff,
reject_h0,
recommendation: recommendation.to_string(),
n_coef: k,
method: "robust".to_string(),
})
}
pub fn classical(
fe: &PanelResult,
re: &RandomEffectsResult,
) -> Result<RobustHausmanResult, GreenersError> {
let k = fe.params.len();
let beta_diff = &fe.params - &re.params;
let var_fe = fe.std_errors.mapv(|s| s.powi(2));
let var_re = re.std_errors.mapv(|s| s.powi(2));
let diff_var = &var_fe - &var_re;
let mut chi2 = 0.0;
for i in 0..k {
if diff_var[i] > 0.0 {
chi2 += beta_diff[i].powi(2) / diff_var[i];
}
}
let dist = ChiSquared::new(k as f64)
.map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
let p_value = 1.0 - dist.cdf(chi2);
let mut cov_diff = Array2::zeros((k, k));
for i in 0..k {
cov_diff[(i, i)] = diff_var[i];
}
let reject_h0 = p_value < 0.05;
let recommendation = if reject_h0 {
"Reject H0. Use FIXED EFFECTS (RE is inconsistent)."
} else {
"Fail to reject H0. Use RANDOM EFFECTS (it is efficient)."
};
Ok(RobustHausmanResult {
chi2,
df: k,
p_value,
beta_diff,
cov_diff,
reject_h0,
recommendation: recommendation.to_string(),
n_coef: k,
method: "classical".to_string(),
})
}
}
pub struct RobustFTest;
impl RobustFTest {
pub fn test(
beta: &Array1<f64>,
vcov: &Array2<f64>,
indices: &[usize],
coef_names: Option<&[String]>,
n: usize,
) -> Result<RobustFTestResult, GreenersError> {
let p = beta.len();
let q = indices.len();
if q == 0 {
return Err(GreenersError::InvalidOperation(
"RobustFTest: need at least 1 coefficient to test".into(),
));
}
if vcov.nrows() != p || vcov.ncols() != p {
return Err(GreenersError::ShapeMismatch(
"RobustFTest: vcov must be p x p".into(),
));
}
for &idx in indices {
if idx >= p {
return Err(GreenersError::InvalidOperation(format!(
"RobustFTest: index {idx} out of range (p={p})"
)));
}
}
let mut beta_r = Array1::zeros(q);
let mut vcov_r = Array2::zeros((q, q));
for (i, &ri) in indices.iter().enumerate() {
beta_r[i] = beta[ri];
for (j, &rj) in indices.iter().enumerate() {
vcov_r[(i, j)] = vcov[(ri, rj)];
}
}
let vcov_r_inv = vcov_r.inv()?;
let wald = beta_r.dot(&vcov_r_inv.dot(&beta_r));
let f_stat = wald / q as f64;
let df_num = q;
let df_denom = n.saturating_sub(p);
let f_dist = FisherSnedecor::new(df_num as f64, df_denom as f64)
.map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
let p_value = 1.0 - f_dist.cdf(f_stat);
let chi2_dist = ChiSquared::new(q as f64)
.map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
let p_value_chi2 = 1.0 - chi2_dist.cdf(wald);
let reject_h0 = p_value < 0.05;
let tested_coefs: Vec<String> = match coef_names {
Some(names) => indices.iter().map(|&i| names[i].clone()).collect(),
None => indices.iter().map(|&i| format!("x{}", i + 1)).collect(),
};
let h0 = format!(
"beta_{} = 0 (jointly)",
tested_coefs.join(" = beta_") + " = 0"
);
Ok(RobustFTestResult {
wald_chi2: wald,
f_stat,
df_num,
df_denom,
p_value,
p_value_chi2,
reject_h0,
h0,
tested_coefs,
n_restrictions: q,
})
}
pub fn test_all_slopes(
beta: &Array1<f64>,
vcov: &Array2<f64>,
coef_names: Option<&[String]>,
n: usize,
) -> Result<RobustFTestResult, GreenersError> {
let p = beta.len();
if p < 2 {
return Err(GreenersError::InvalidOperation(
"RobustFTest: need at least 2 coefficients".into(),
));
}
let indices: Vec<usize> = (1..p).collect();
Self::test(beta, vcov, &indices, coef_names, n)
}
}