use crate::linalg::LinalgInverse as _;
use crate::GreenersError;
use ndarray::{Array1, Array2};
use statrs::distribution::{ContinuousCDF, Normal};
use std::fmt;
#[derive(Debug)]
pub struct FaPanelResult {
pub beta: Array1<f64>,
pub gamma: Array1<f64>,
pub beta_se: Array1<f64>,
pub gamma_se: Array1<f64>,
pub beta_t: Array1<f64>,
pub gamma_t: Array1<f64>,
pub beta_p: Array1<f64>,
pub gamma_p: Array1<f64>,
pub factors: Array2<f64>,
pub r_squared: f64,
pub n_obs: usize,
pub n_entities: usize,
pub n_regressors: usize,
pub n_factors: usize,
pub regressor_names: Vec<String>,
pub factor_names: Vec<String>,
}
impl fmt::Display for FaPanelResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n{:=^78}", " Factor-Augmented Panel ")?;
writeln!(f, "PCA factors + FE panel regression")?;
writeln!(f, "{:<20} {:>12}", "Observations:", self.n_obs)?;
writeln!(f, "{:<20} {:>12}", "Entities:", self.n_entities)?;
writeln!(f, "{:<20} {:>12}", "Regressors:", self.n_regressors)?;
writeln!(f, "{:<20} {:>12}", "Factors:", self.n_factors)?;
writeln!(f, "{:<20} {:>12.6}", "R-squared:", self.r_squared)?;
writeln!(f, "\n{:-^78}", "")?;
writeln!(
f,
"{:<14} {:>12} {:>12} {:>10} {:>10}",
"Variable", "Coef.", "Std.Err.", "t", "P>|t|"
)?;
writeln!(f, "{:-^78}", "")?;
for i in 0..self.n_regressors {
let name = self
.regressor_names
.get(i)
.cloned()
.unwrap_or_else(|| format!("x{}", i));
writeln!(
f,
"{:<14} {:>12.6} {:>12.6} {:>10.3} {:>10.4}",
name, self.beta[i], self.beta_se[i], self.beta_t[i], self.beta_p[i]
)?;
}
for i in 0..self.n_factors {
let name = self
.factor_names
.get(i)
.cloned()
.unwrap_or_else(|| format!("F{}", i + 1));
writeln!(
f,
"{:<14} {:>12.6} {:>12.6} {:>10.3} {:>10.4}",
name, self.gamma[i], self.gamma_se[i], self.gamma_t[i], self.gamma_p[i]
)?;
}
write!(f, "{:=^78}", "")
}
}
pub struct FAPanel;
impl FAPanel {
pub fn fit(
y: &Array1<f64>,
x: &Array2<f64>,
aux: &Array2<f64>,
entity_ids: &[i64],
period_ids: &[i64],
n_factors: usize,
regressor_names: Option<Vec<String>>,
) -> Result<FaPanelResult, GreenersError> {
let n = y.len();
let k = x.ncols();
let t_aux = aux.nrows();
let n_aux = aux.ncols();
if x.nrows() != n || entity_ids.len() != n || period_ids.len() != n {
return Err(GreenersError::ShapeMismatch(
"FAPanel: dimension mismatch".into(),
));
}
if n_factors == 0 || n_factors >= n_aux {
return Err(GreenersError::InvalidOperation(
"FAPanel: invalid n_factors".into(),
));
}
let names = regressor_names.unwrap_or_else(|| (0..k).map(|i| format!("x{}", i)).collect());
let factors = Self::pca_factors(aux, n_factors, t_aux)?;
let mut unique_periods: Vec<i64> = period_ids
.iter()
.copied()
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
unique_periods.sort();
if unique_periods.len() != t_aux {
return Err(GreenersError::InvalidOperation(
"FAPanel: period count must match aux rows".into(),
));
}
let period_to_factor: std::collections::HashMap<i64, Array1<f64>> = unique_periods
.iter()
.enumerate()
.map(|(i, &p)| (p, factors.row(i).to_owned()))
.collect();
let mut entity_sums: std::collections::HashMap<i64, (f64, usize)> =
std::collections::HashMap::new();
for i in 0..n {
let entry = entity_sums.entry(entity_ids[i]).or_insert((0.0, 0));
entry.0 += y[i];
entry.1 += 1;
}
let entity_means: std::collections::HashMap<i64, f64> = entity_sums
.iter()
.map(|(&k, &(s, c))| (k, s / c as f64))
.collect();
let mut y_dm = Array1::zeros(n);
for i in 0..n {
y_dm[i] = y[i] - entity_means[&entity_ids[i]];
}
let n_total_reg = k + n_factors;
let mut z_dm = Array2::zeros((n, n_total_reg));
for j in 0..k {
let mut x_sums: std::collections::HashMap<i64, (f64, usize)> =
std::collections::HashMap::new();
for i in 0..n {
let xe = x_sums.entry(entity_ids[i]).or_insert((0.0, 0));
xe.0 += x[(i, j)];
xe.1 += 1;
}
for i in 0..n {
let xm = x_sums[&entity_ids[i]].0 / x_sums[&entity_ids[i]].1 as f64;
z_dm[(i, j)] = x[(i, j)] - xm;
}
}
for j in 0..n_factors {
let mut f_sums: std::collections::HashMap<i64, (f64, usize)> =
std::collections::HashMap::new();
for i in 0..n {
let f_val = period_to_factor[&period_ids[i]][j];
let fe = f_sums.entry(entity_ids[i]).or_insert((0.0, 0));
fe.0 += f_val;
fe.1 += 1;
}
for i in 0..n {
let f_val = period_to_factor[&period_ids[i]][j];
let fm = f_sums[&entity_ids[i]].0 / f_sums[&entity_ids[i]].1 as f64;
z_dm[(i, k + j)] = f_val - fm;
}
}
let zt = z_dm.t();
let ztz = zt.dot(&z_dm);
let ztz_inv = (&ztz + Array2::<f64>::eye(n_total_reg) * 1e-8).inv()?;
let zty = zt.dot(&y_dm);
let beta_full: Array1<f64> = ztz_inv.dot(&zty);
let residuals = &y_dm - z_dm.dot(&beta_full);
let sse = residuals.dot(&residuals);
let n_entities = entity_sums.len();
let sigma2 = sse / (n - n_entities - n_total_reg) as f64;
let cov = &ztz_inv * sigma2;
let se = cov.diag().mapv(|v| v.sqrt());
let tv = &beta_full / &se;
let normal =
Normal::new(0.0, 1.0).map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
let pv = tv.mapv(|t| 2.0 * (1.0 - normal.cdf(t.abs())));
let beta = beta_full.slice(ndarray::s![0..k]).to_owned();
let gamma = beta_full.slice(ndarray::s![k..n_total_reg]).to_owned();
let beta_se = se.slice(ndarray::s![0..k]).to_owned();
let gamma_se = se.slice(ndarray::s![k..n_total_reg]).to_owned();
let beta_t = tv.slice(ndarray::s![0..k]).to_owned();
let gamma_t = tv.slice(ndarray::s![k..n_total_reg]).to_owned();
let beta_p = pv.slice(ndarray::s![0..k]).to_owned();
let gamma_p = pv.slice(ndarray::s![k..n_total_reg]).to_owned();
let y_mean = y_dm.mean().unwrap_or(0.0);
let tss = y_dm.mapv(|v| (v - y_mean).powi(2)).sum();
let r_squared = if tss > 1e-15 { 1.0 - sse / tss } else { 0.0 };
let factor_names: Vec<String> = (0..n_factors).map(|i| format!("F{}", i + 1)).collect();
Ok(FaPanelResult {
beta,
gamma,
beta_se,
gamma_se,
beta_t,
gamma_t,
beta_p,
gamma_p,
factors,
r_squared,
n_obs: n,
n_entities,
n_regressors: k,
n_factors,
regressor_names: names,
factor_names,
})
}
fn pca_factors(
aux: &Array2<f64>,
n_factors: usize,
t: usize,
) -> Result<Array2<f64>, GreenersError> {
let n_aux = aux.ncols();
let mut aux_std = Array2::zeros((t, n_aux));
for j in 0..n_aux {
let mean: f64 = aux.column(j).mean().unwrap_or(0.0);
let std_val: f64 = aux.column(j).std(0.0).max(1e-10);
for i in 0..t {
aux_std[(i, j)] = (aux[(i, j)] - mean) / std_val;
}
}
let aux_t = aux_std.t();
let cov = aux_t.dot(&aux_std) / t as f64;
let mut factors = Array2::zeros((t, n_factors));
let mut remaining = cov.clone();
for f in 0..n_factors {
let mut v = Array1::ones(n_aux) / (n_aux as f64).sqrt();
for _ in 0..100 {
let v_new = remaining.dot(&v);
let norm = v_new.mapv(|x| x * x).sum().sqrt().max(1e-10);
v = v_new / norm;
}
let lambda = v.dot(&remaining.dot(&v));
for i in 0..t {
factors[(i, f)] = aux_std.row(i).dot(&v);
}
for a in 0..n_aux {
for b in 0..n_aux {
remaining[(a, b)] -= lambda * v[a] * v[b];
}
}
}
Ok(factors)
}
}