use ndarray::{Array1, Array2};
type ModelStats = (f64, f64, f64, f64, f64, f64, f64, usize);
type MundlakResult = (f64, f64, usize, Vec<f64>, Vec<f64>);
pub struct ModelSelection;
impl ModelSelection {
pub fn compare_models(
models: Vec<(&str, f64, usize, usize)>,
) -> Vec<(String, f64, f64, usize, usize)> {
let mut results: Vec<(String, f64, f64)> = models
.iter()
.map(|(name, loglik, k, n)| {
let aic = -2.0 * loglik + 2.0 * (*k as f64);
let bic = -2.0 * loglik + (*k as f64) * (*n as f64).ln();
(name.to_string(), aic, bic)
})
.collect();
results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
let mut bic_sorted = results.clone();
bic_sorted.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap());
results
.iter()
.map(|(name, aic, bic)| {
let rank_aic = results.iter().position(|x| &x.0 == name).unwrap() + 1;
let rank_bic = bic_sorted.iter().position(|x| &x.0 == name).unwrap() + 1;
(name.clone(), *aic, *bic, rank_aic, rank_bic)
})
.collect()
}
pub fn akaike_weights(aic_values: &[f64]) -> (Vec<f64>, Vec<f64>) {
let min_aic = aic_values.iter().cloned().fold(f64::INFINITY, f64::min);
let delta_aic: Vec<f64> = aic_values.iter().map(|aic| aic - min_aic).collect();
let rel_likelihood: Vec<f64> = delta_aic.iter().map(|d| (-d / 2.0).exp()).collect();
let sum_rel: f64 = rel_likelihood.iter().sum();
let weights: Vec<f64> = rel_likelihood.iter().map(|r| r / sum_rel).collect();
(delta_aic, weights)
}
pub fn print_comparison(comparison: &[(String, f64, f64, usize, usize)]) {
println!("\n{:=^80}", " Model Comparison ");
println!("{:-^80}", "");
println!(
"{:<20} | {:>12} | {:>12} | {:>8} | {:>8}",
"Model", "AIC", "BIC", "Rank(AIC)", "Rank(BIC)"
);
println!("{:-^80}", "");
for (name, aic, bic, rank_aic, rank_bic) in comparison {
println!(
"{:<20} | {:>12.2} | {:>12.2} | {:>8} | {:>8}",
name, aic, bic, rank_aic, rank_bic
);
}
println!("{:=^80}", "");
}
}
pub struct PanelDiagnostics;
impl PanelDiagnostics {
pub fn breusch_pagan_lm(
residuals_pooled: &Array1<f64>,
entity_ids: &[usize],
) -> Result<(f64, f64), String> {
use statrs::distribution::{ChiSquared, ContinuousCDF};
use std::collections::HashMap;
let n = residuals_pooled.len();
if entity_ids.len() != n {
return Err("Entity IDs length must match residuals length".to_string());
}
let mut entity_residuals: HashMap<usize, Vec<f64>> = HashMap::new();
for (i, &entity_id) in entity_ids.iter().enumerate() {
entity_residuals
.entry(entity_id)
.or_default()
.push(residuals_pooled[i]);
}
let n_entities = entity_residuals.len();
let t_bar = n as f64 / n_entities as f64;
let mut sum_squared_means = 0.0;
let mut sum_squared_residuals = 0.0;
for residuals in entity_residuals.values() {
let mean: f64 = residuals.iter().sum::<f64>() / residuals.len() as f64;
let t = residuals.len() as f64;
sum_squared_means += t * mean.powi(2);
for &r in residuals {
sum_squared_residuals += r.powi(2);
}
}
let lm_stat = (n as f64 / 2.0)
* ((sum_squared_means / sum_squared_residuals) - 1.0).powi(2)
/ (t_bar - 1.0);
let chi2_dist = ChiSquared::new(1.0).map_err(|e| e.to_string())?;
let p_value = 1.0 - chi2_dist.cdf(lm_stat);
Ok((lm_stat, p_value))
}
pub fn f_test_fixed_effects(
ssr_pooled: f64,
ssr_fe: f64,
n: usize,
n_entities: usize,
k: usize,
) -> Result<(f64, f64), String> {
use statrs::distribution::{ContinuousCDF, FisherSnedecor};
if n <= n_entities + k {
return Err("Insufficient degrees of freedom".to_string());
}
let df_num = n_entities - 1; let df_denom = n - n_entities - k;
let f_stat = ((ssr_pooled - ssr_fe) / df_num as f64) / (ssr_fe / df_denom as f64);
let f_dist =
FisherSnedecor::new(df_num as f64, df_denom as f64).map_err(|e| e.to_string())?;
let p_value = 1.0 - f_dist.cdf(f_stat);
Ok((f_stat, p_value))
}
pub fn arellano_bond_test(
y: &Array1<f64>,
x: &Array2<f64>,
entity_ids: &[i64],
time_vals: &[f64],
) -> Result<(f64, f64, f64, f64), String> {
use crate::{CovarianceType, OLS};
use statrs::distribution::{ContinuousCDF, Normal};
use std::collections::HashMap;
let n = y.len();
let k = x.ncols();
if entity_ids.len() != n || time_vals.len() != n {
return Err("entity_ids e time_vals devem ter o mesmo comprimento que y".to_string());
}
let mut entity_idx: HashMap<i64, Vec<usize>> = HashMap::new();
for (i, &eid) in entity_ids.iter().enumerate() {
entity_idx.entry(eid).or_default().push(i);
}
for indices in entity_idx.values_mut() {
indices.sort_by(|&a, &b| {
time_vals[a]
.partial_cmp(&time_vals[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
}
let mut sorted_entities: Vec<i64> = entity_idx.keys().copied().collect();
sorted_entities.sort();
let mut dy_vec: Vec<f64> = Vec::new();
let mut dx_rows: Vec<Vec<f64>> = Vec::new();
for eid in &sorted_entities {
let indices = &entity_idx[eid];
let t = indices.len();
if t < 2 {
continue;
}
for s in 1..t {
let curr = indices[s];
let prev = indices[s - 1];
dy_vec.push(y[curr] - y[prev]);
dx_rows.push((0..k).map(|c| x[[curr, c]] - x[[prev, c]]).collect());
}
}
let n_fd = dy_vec.len();
if n_fd == 0 {
return Err("Nenhuma observação após primeira diferença".to_string());
}
let dy = Array1::from_vec(dy_vec);
let mut dx = Array2::<f64>::zeros((n_fd, k));
for (i, row) in dx_rows.iter().enumerate() {
for (j, &v) in row.iter().enumerate() {
dx[[i, j]] = v;
}
}
let active_cols: Vec<usize> = (0..k)
.filter(|&c| dx.column(c).iter().any(|&v| v.abs() > 1e-12))
.collect();
let dx_active = {
let mut m = Array2::<f64>::zeros((n_fd, active_cols.len()));
for (nc, &oc) in active_cols.iter().enumerate() {
m.column_mut(nc).assign(&dx.column(oc));
}
m
};
let fd_ols = OLS::fit(&dy, &dx_active, CovarianceType::NonRobust)
.map_err(|e| format!("OLS na primeira diferença: {e}"))?;
let fd_resid = fd_ols.residuals(&dy, &dx_active);
let mut entity_fd_resid: HashMap<i64, Vec<f64>> = HashMap::new();
let mut row_ptr = 0usize;
for eid in &sorted_entities {
let t = entity_idx[eid].len();
if t < 2 {
continue;
}
let fd_count = t - 1;
let resids: Vec<f64> = (row_ptr..row_ptr + fd_count).map(|i| fd_resid[i]).collect();
entity_fd_resid.insert(*eid, resids);
row_ptr += fd_count;
}
let m_stat = |p: usize| -> Option<(f64, f64)> {
let mut c_p = 0.0f64;
let mut v_p = 0.0f64;
for resids in entity_fd_resid.values() {
let m = resids.len();
if m <= p {
continue;
}
let entity_sum: f64 = (p..m).map(|t| resids[t] * resids[t - p]).sum();
c_p += entity_sum;
v_p += entity_sum * entity_sum; }
if v_p < 1e-20 {
return None; }
let stat = c_p / v_p.sqrt();
let normal = Normal::new(0.0, 1.0).ok()?;
let pval = 2.0 * (1.0 - normal.cdf(stat.abs()));
Some((stat, pval))
};
let (m1, p1) = m_stat(1)
.ok_or("Dados insuficientes para m1 (precisa T ≥ 3 por entidade)".to_string())?;
let (m2, p2) = m_stat(2)
.ok_or("Dados insuficientes para m2 (precisa T ≥ 4 por entidade)".to_string())?;
Ok((m1, p1, m2, p2))
}
pub fn chamberlain(
y: &Array1<f64>,
x: &Array2<f64>,
entity_ids: &[i64],
time_vals: &[f64],
) -> Result<(f64, f64, usize, usize, usize, usize), String> {
use crate::{CovarianceType, OLS};
use statrs::distribution::{ContinuousCDF, FisherSnedecor};
use std::collections::HashMap;
let n = y.len();
let k_full = x.ncols();
if entity_ids.len() != n || time_vals.len() != n {
return Err("entity_ids e time_vals devem ter o mesmo comprimento que y".to_string());
}
let non_const_cols: Vec<usize> = (0..k_full)
.filter(|&c| {
let mean = x.column(c).sum() / n as f64;
x.column(c).iter().any(|&v| (v - mean).abs() > 1e-10)
})
.collect();
let k = non_const_cols.len();
if k == 0 {
return Err("Nenhum regressor variante no tempo encontrado".to_string());
}
let mut seen_bits: std::collections::HashSet<u64> = std::collections::HashSet::new();
let mut unique_times: Vec<f64> = Vec::new();
for &t in time_vals {
if seen_bits.insert(t.to_bits()) {
unique_times.push(t);
}
}
unique_times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let t_count = unique_times.len();
if t_count < 2 {
return Err("O teste de Chamberlain requer pelo menos 2 períodos".to_string());
}
let time_to_idx: HashMap<u64, usize> = unique_times
.iter()
.enumerate()
.map(|(i, &t)| (t.to_bits(), i))
.collect();
let mut entity_period: HashMap<i64, HashMap<usize, Vec<f64>>> = HashMap::new();
for (obs, &eid) in entity_ids.iter().enumerate() {
let t_idx = *time_to_idx
.get(&time_vals[obs].to_bits())
.ok_or("Período não encontrado no índice")?;
let vals: Vec<f64> = non_const_cols.iter().map(|&c| x[[obs, c]]).collect();
entity_period.entry(eid).or_default().insert(t_idx, vals);
}
let n_entities = entity_period.len();
for (&eid, periods) in &entity_period {
if periods.len() != t_count {
return Err(format!(
"Painel desbalanceado: entidade {} tem {} períodos (esperado {}). \
Filtre o dataset para incluir apenas entidades com todos os {} períodos.",
eid,
periods.len(),
t_count,
t_count
));
}
}
let k_chamber = k * t_count;
let k_aug_total = k_full + k_chamber;
let mut x_aug = Array2::<f64>::zeros((n, k_aug_total));
for i in 0..n {
for c in 0..k_full {
x_aug[[i, c]] = x[[i, c]];
}
let eid = entity_ids[i];
let ep = entity_period
.get(&eid)
.ok_or("ID de entidade não encontrado")?;
for (j, _) in non_const_cols.iter().enumerate() {
for s in 0..t_count {
if let Some(vals) = ep.get(&s) {
x_aug[[i, k_full + j * t_count + s]] = vals[j];
}
}
}
}
let active_aug: Vec<usize> = (k_full..k_aug_total)
.filter(|&c| {
let mean = x_aug.column(c).sum() / n as f64;
x_aug.column(c).iter().any(|&v| (v - mean).abs() > 1e-10)
})
.collect();
let k_active = active_aug.len();
if k_active == 0 {
return Err(
"Nenhuma coluna de augmentação com variância — regressores constantes?".to_string(),
);
}
let k_final = k_full + k_active;
if n <= k_final {
return Err(format!(
"Graus de liberdade insuficientes: n={} ≤ colunas do modelo augmentado={}. \
T muito grande relativo ao número de observações.",
n, k_final
));
}
let mut x_final = Array2::<f64>::zeros((n, k_final));
for i in 0..n {
for c in 0..k_full {
x_final[[i, c]] = x[[i, c]];
}
for (new_c, &old_c) in active_aug.iter().enumerate() {
x_final[[i, k_full + new_c]] = x_aug[[i, old_c]];
}
}
let ols_r =
OLS::fit(y, x, CovarianceType::NonRobust).map_err(|e| format!("OLS restrito: {e}"))?;
let ols_u = OLS::fit(y, &x_final, CovarianceType::NonRobust)
.map_err(|e| format!("OLS não-restrito: {e}"))?;
let ssr_r = ols_r.sigma.powi(2) * ols_r.df_resid as f64;
let ssr_u = ols_u.sigma.powi(2) * ols_u.df_resid as f64;
let df_u = ols_u.df_resid;
if df_u == 0 || ssr_u < 1e-15 {
return Err("Graus de liberdade insuficientes no modelo não-restrito".to_string());
}
let f_stat = ((ssr_r - ssr_u) / k_active as f64) / (ssr_u / df_u as f64);
let f_dist = FisherSnedecor::new(k_active as f64, df_u as f64)
.map_err(|e| format!("F-distribuição: {e}"))?;
let p_value = 1.0 - f_dist.cdf(f_stat.max(0.0));
Ok((f_stat, p_value, k_active, df_u, n_entities, t_count))
}
pub fn mundlak(
y: &Array1<f64>,
x: &Array2<f64>,
entity_ids: &[i64],
) -> Result<MundlakResult, String> {
use crate::{CovarianceType, OLS};
use statrs::distribution::{ContinuousCDF, FisherSnedecor};
use std::collections::HashMap;
let n = y.len();
let k_full = x.ncols();
if entity_ids.len() != n {
return Err("entity_ids deve ter o mesmo comprimento que y".to_string());
}
let non_const_cols: Vec<usize> = (0..k_full)
.filter(|&c| {
let col: Vec<f64> = x.column(c).to_vec();
let mean = col.iter().sum::<f64>() / col.len() as f64;
col.iter().any(|&v| (v - mean).abs() > 1e-10)
})
.collect();
let k = non_const_cols.len();
if k == 0 {
return Err("Nenhum regressor variante no tempo encontrado".to_string());
}
let mut entity_sums: HashMap<i64, (Vec<f64>, usize)> = HashMap::new();
for (i, &eid) in entity_ids.iter().enumerate() {
let entry = entity_sums.entry(eid).or_insert_with(|| (vec![0.0; k], 0));
for (j, &c) in non_const_cols.iter().enumerate() {
entry.0[j] += x[[i, c]];
}
entry.1 += 1;
}
let entity_means: HashMap<i64, Vec<f64>> = entity_sums
.into_iter()
.map(|(eid, (sums, cnt))| (eid, sums.into_iter().map(|s| s / cnt as f64).collect()))
.collect();
let mut x_aug = Array2::<f64>::zeros((n, k_full + k));
for i in 0..n {
for c in 0..k_full {
x_aug[[i, c]] = x[[i, c]];
}
let means = &entity_means[&entity_ids[i]];
for (j, &mean) in means.iter().enumerate() {
x_aug[[i, k_full + j]] = mean;
}
}
let ols_r = OLS::fit(y, x, CovarianceType::NonRobust)
.map_err(|e| format!("OLS restrito falhou: {e}"))?;
let ols_u = OLS::fit(y, &x_aug, CovarianceType::NonRobust)
.map_err(|e| format!("OLS não-restrito falhou: {e}"))?;
let ssr_r = ols_r.sigma.powi(2) * ols_r.df_resid as f64;
let ssr_u = ols_u.sigma.powi(2) * ols_u.df_resid as f64;
let df_u = ols_u.df_resid;
if df_u == 0 || ssr_u < 1e-15 {
return Err("Graus de liberdade insuficientes no modelo não-restrito".to_string());
}
let f_stat = ((ssr_r - ssr_u) / k as f64) / (ssr_u / df_u as f64);
let f_dist = FisherSnedecor::new(k as f64, df_u as f64)
.map_err(|e| format!("F-distribuição: {e}"))?;
let p_value = 1.0 - f_dist.cdf(f_stat.max(0.0));
let n_params = ols_u.params.len();
let gamma_hat: Vec<f64> = (n_params - k..n_params).map(|i| ols_u.params[i]).collect();
let gamma_se: Vec<f64> = (n_params - k..n_params)
.map(|i| ols_u.std_errors[i])
.collect();
Ok((f_stat, p_value, k, gamma_hat, gamma_se))
}
pub fn wooldridge_serial(
y: &Array1<f64>,
x: &Array2<f64>,
entity_ids: &[i64],
time_vals: &[f64],
) -> Result<(f64, f64, f64, usize), String> {
use crate::{CovarianceType, OLS};
use statrs::distribution::{ContinuousCDF, StudentsT};
use std::collections::HashMap;
let n = y.len();
if entity_ids.len() != n || time_vals.len() != n {
return Err("entity_ids e time_vals devem ter o mesmo comprimento que y".to_string());
}
let k = x.ncols();
if n < 4 {
return Err("Observações insuficientes para o teste de Wooldridge".to_string());
}
let mut entity_idx: HashMap<i64, Vec<usize>> = HashMap::new();
for (i, &eid) in entity_ids.iter().enumerate() {
entity_idx.entry(eid).or_default().push(i);
}
for indices in entity_idx.values_mut() {
indices.sort_by(|&a, &b| {
time_vals[a]
.partial_cmp(&time_vals[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
}
let n_entities = entity_idx.len();
let mut sorted_entities: Vec<i64> = entity_idx.keys().copied().collect();
sorted_entities.sort();
let mut dy_vec: Vec<f64> = Vec::new();
let mut dx_rows: Vec<Vec<f64>> = Vec::new();
for eid in &sorted_entities {
let indices = &entity_idx[eid];
let t = indices.len();
if t < 2 {
continue;
}
for s in 1..t {
let i_curr = indices[s];
let i_prev = indices[s - 1];
dy_vec.push(y[i_curr] - y[i_prev]);
let row: Vec<f64> = (0..k).map(|c| x[[i_curr, c]] - x[[i_prev, c]]).collect();
dx_rows.push(row);
}
}
let n_fd = dy_vec.len();
if n_fd == 0 {
return Err("Nenhuma observação após primeira diferença".to_string());
}
let dy = Array1::from_vec(dy_vec);
let mut dx = Array2::<f64>::zeros((n_fd, k));
for (i, row) in dx_rows.iter().enumerate() {
for (j, &v) in row.iter().enumerate() {
dx[[i, j]] = v;
}
}
let active_cols: Vec<usize> = (0..k)
.filter(|&c| dx.column(c).iter().any(|&v| v.abs() > 1e-12))
.collect();
if active_cols.is_empty() {
return Err("Todos os regressores tornaram-se zero após diferenciação".to_string());
}
let dx_active = {
let mut m = Array2::<f64>::zeros((n_fd, active_cols.len()));
for (new_c, &old_c) in active_cols.iter().enumerate() {
m.column_mut(new_c).assign(&dx.column(old_c));
}
m
};
let fd_ols = OLS::fit(&dy, &dx_active, CovarianceType::NonRobust)
.map_err(|e| format!("OLS na primeira diferença falhou: {e}"))?;
let fd_resid = fd_ols.residuals(&dy, &dx_active);
let mut aux_curr: Vec<f64> = Vec::new();
let mut aux_lag: Vec<f64> = Vec::new();
let mut row_ptr = 0usize;
for eid in &sorted_entities {
let t = entity_idx[eid].len();
if t < 2 {
continue;
}
let fd_count = t - 1;
if fd_count >= 2 {
for s in 1..fd_count {
aux_curr.push(fd_resid[row_ptr + s]);
aux_lag.push(fd_resid[row_ptr + s - 1]);
}
}
row_ptr += fd_count;
}
let n_pairs = aux_curr.len();
if n_pairs < 2 {
return Err(
"Poucas observações para o teste (necessário T ≥ 3 em pelo menos uma entidade)"
.to_string(),
);
}
let sum_xx: f64 = aux_lag.iter().map(|&v| v * v).sum();
let sum_xy: f64 = aux_lag
.iter()
.zip(aux_curr.iter())
.map(|(&xl, &xc)| xl * xc)
.sum();
if sum_xx < 1e-15 {
return Err("Matriz singular na regressão auxiliar".to_string());
}
let rho_hat = sum_xy / sum_xx;
let ssr_aux: f64 = aux_lag
.iter()
.zip(aux_curr.iter())
.map(|(&xl, &xc)| (xc - rho_hat * xl).powi(2))
.sum();
let df_aux = (n_pairs - 1) as f64;
let se_rho = (ssr_aux / df_aux / sum_xx).sqrt();
if se_rho < 1e-15 {
return Err("Erro padrão de ρ̂ próximo de zero".to_string());
}
let t_stat = (rho_hat - (-0.5)) / se_rho;
let df_t = (n_entities - 1) as f64;
let t_dist = StudentsT::new(0.0, 1.0, df_t).map_err(|e| format!("t-distribuição: {e}"))?;
let p_value = 2.0 * (1.0 - t_dist.cdf(t_stat.abs()));
Ok((rho_hat, t_stat, p_value, n_pairs))
}
pub fn pesaran_cd(residuals: &Array1<f64>, entity_ids: &[usize]) -> Result<(f64, f64), String> {
use statrs::distribution::{ContinuousCDF, Normal};
use std::collections::HashMap;
let n_obs = residuals.len();
if entity_ids.len() != n_obs {
return Err("entity_ids length must match residuals length".to_string());
}
let mut groups: HashMap<usize, Vec<f64>> = HashMap::new();
for (&id, &r) in entity_ids.iter().zip(residuals.iter()) {
groups.entry(id).or_default().push(r);
}
let mut entity_list: Vec<usize> = groups.keys().copied().collect();
entity_list.sort();
let n_entities = entity_list.len();
if n_entities < 2 {
return Err("Need at least 2 entities for the Pesaran CD test".to_string());
}
let residuals_by_entity: Vec<&Vec<f64>> =
entity_list.iter().map(|id| &groups[id]).collect();
let mut cd_sum = 0.0;
let mut n_pairs = 0usize;
for (i, &ei) in residuals_by_entity.iter().enumerate() {
for &ej in residuals_by_entity.iter().skip(i + 1) {
let t_ij = ei.len().min(ej.len());
if t_ij < 2 {
continue;
}
let mean_i = ei[..t_ij].iter().sum::<f64>() / t_ij as f64;
let mean_j = ej[..t_ij].iter().sum::<f64>() / t_ij as f64;
let cov: f64 = ei[..t_ij]
.iter()
.zip(ej[..t_ij].iter())
.map(|(&a, &b)| (a - mean_i) * (b - mean_j))
.sum();
let var_i: f64 = ei[..t_ij].iter().map(|&a| (a - mean_i).powi(2)).sum();
let var_j: f64 = ej[..t_ij].iter().map(|&b| (b - mean_j).powi(2)).sum();
let denom = (var_i * var_j).sqrt();
if denom < 1e-15 {
continue;
}
cd_sum += (t_ij as f64).sqrt() * (cov / denom);
n_pairs += 1;
}
}
if n_pairs == 0 {
return Err("No valid entity pairs found for CD test".to_string());
}
let cd = (2.0 / (n_entities * (n_entities - 1)) as f64).sqrt() * cd_sum;
let normal = Normal::new(0.0, 1.0).map_err(|e| e.to_string())?;
let p_value = 2.0 * (1.0 - normal.cdf(cd.abs()));
Ok((cd, p_value))
}
}
pub struct SummaryStats;
impl SummaryStats {
pub fn describe(data: &Array1<f64>) -> (f64, f64, f64, f64, f64, f64, f64, usize) {
let n = data.len();
let mean = data.mean().unwrap_or(0.0);
let std = data.std(0.0);
let mut sorted = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let min = sorted[0];
let max = sorted[n - 1];
let q25 = Self::percentile(&sorted, 25.0);
let median = Self::percentile(&sorted, 50.0);
let q75 = Self::percentile(&sorted, 75.0);
(mean, std, min, q25, median, q75, max, n)
}
fn percentile(sorted_data: &[f64], p: f64) -> f64 {
let n = sorted_data.len();
let idx = (p / 100.0) * (n - 1) as f64;
let lower = idx.floor() as usize;
let upper = idx.ceil() as usize;
let weight = idx - lower as f64;
sorted_data[lower] * (1.0 - weight) + sorted_data[upper] * weight
}
pub fn print_summary(stats: &[(&str, ModelStats)]) {
println!("\n{:=^90}", " Descriptive Statistics ");
println!("{:-^90}", "");
println!(
"{:<12} | {:>8} | {:>8} | {:>8} | {:>8} | {:>8} | {:>8} | {:>8}",
"Variable", "Mean", "Std", "Min", "Q25", "Median", "Q75", "Max"
);
println!("{:-^90}", "");
for (name, (mean, std, min, q25, median, q75, max, _n)) in stats {
println!(
"{:<12} | {:>8.2} | {:>8.2} | {:>8.2} | {:>8.2} | {:>8.2} | {:>8.2} | {:>8.2}",
name, mean, std, min, q25, median, q75, max
);
}
println!("{:=^90}", "");
}
}