use crate::error::GreenersError;
use crate::DataFrame;
use ndarray::{Array1, Array2};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug)]
pub struct SynthResult {
pub weights: Vec<(String, f64)>,
pub synthetic_series: Vec<f64>,
pub actual_series: Vec<f64>,
pub time_index: Vec<f64>,
pub t0: f64,
pub rmspe_pre: f64,
pub rmspe_post: Option<f64>,
pub treated_unit: String,
pub outcome_name: String,
pub n_donors: usize,
pub t_pre: usize,
pub t_post: usize,
}
impl fmt::Display for SynthResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let thick = "═".repeat(68);
let thin = "─".repeat(68);
writeln!(f, "\n{thick}")?;
writeln!(
f,
" Controle Sintético — Abadie, Diamond, Hainmueller (2010)"
)?;
writeln!(f, "{thick}")?;
writeln!(
f,
" Unidade tratada : {} Outcome: {}",
self.treated_unit, self.outcome_name
)?;
writeln!(
f,
" T₀ (1ª pós-trat): {} Doadores: {} T pré: {} T pós: {}",
self.t0, self.n_donors, self.t_pre, self.t_post
)?;
writeln!(f, " RMSPE pré : {:.4}", self.rmspe_pre)?;
if let Some(rp) = self.rmspe_post {
let ratio = if self.rmspe_pre > 1e-12 {
rp / self.rmspe_pre
} else {
f64::NAN
};
writeln!(f, " RMSPE pós : {:.4} razão pós/pré: {:.3}", rp, ratio)?;
}
writeln!(f, "{thin}")?;
writeln!(f, " Pesos dos doadores (w > 0.001):")?;
let mut nonzero: Vec<&(String, f64)> =
self.weights.iter().filter(|(_, w)| *w > 0.001).collect();
nonzero.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
if nonzero.is_empty() {
writeln!(f, " (nenhum peso > 0.001)")?;
} else {
for (unit, w) in &nonzero {
writeln!(f, " {:<24} {:.4}", unit, w)?;
}
}
writeln!(f, "{thin}")?;
writeln!(
f,
" {:>8} {:>12} {:>12} {:>12}",
"Período", "Real", "Sintético", "Efeito"
)?;
writeln!(f, " {}", "─".repeat(50))?;
for (i, &t) in self.time_index.iter().enumerate() {
let actual = self.actual_series[i];
let synth = self.synthetic_series[i];
let post = t >= self.t0;
let effect_str = if post {
format!("{:>12.4}", actual - synth)
} else {
" ".to_string()
};
let marker = if post { " *" } else { " " };
writeln!(
f,
"{marker}{:>8.0} {:>12.4} {:>12.4} {}",
t, actual, synth, effect_str
)?;
}
writeln!(f, "{thick}")?;
writeln!(f, " * pós-tratamento")
}
}
pub struct SyntheticControl;
impl SyntheticControl {
pub fn fit(
outcome_col: &str,
treated_unit: &str,
t0: f64,
df: &DataFrame,
id_col: &str,
time_col: &str,
covariate_cols: Option<&[String]>,
) -> Result<SynthResult, GreenersError> {
let y_col = df.get(outcome_col)?.to_owned();
let t_col = df.get(time_col)?.to_owned();
let n = df.n_rows();
let unit_ids: Vec<String> = unit_ids_as_strings(df, id_col)?;
let mut times: Vec<f64> = t_col.to_vec();
times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
times.dedup_by(|a, b| (*a - *b).abs() < 1e-9);
let n_t = times.len();
let time_idx: HashMap<String, usize> = times
.iter()
.enumerate()
.map(|(i, &t)| (float_key(t), i))
.collect();
let mut units: Vec<String> = unit_ids.clone();
units.sort();
units.dedup();
let n_units = units.len();
let unit_idx: HashMap<&str, usize> = units
.iter()
.enumerate()
.map(|(i, u)| (u.as_str(), i))
.collect();
let treated_j = *unit_idx.get(treated_unit).ok_or_else(|| {
GreenersError::InvalidOperation(format!(
"synth: unidade tratada '{treated_unit}' não encontrada em '{id_col}'"
))
})?;
let mut y_mat = vec![f64::NAN; n_t * n_units];
for i in 0..n {
let t_key = float_key(t_col[i]);
let t_i = *time_idx.get(&t_key).ok_or_else(|| {
GreenersError::InvalidOperation("synth: tempo inconsistente".into())
})?;
let u_i = *unit_idx.get(unit_ids[i].as_str()).ok_or_else(|| {
GreenersError::InvalidOperation("synth: unidade inconsistente".into())
})?;
y_mat[t_i * n_units + u_i] = y_col[i];
}
if y_mat.iter().any(|v| v.is_nan()) {
return Err(GreenersError::InvalidOperation(
"synth: painel desbalanceado ou missing — preencha antes de estimar".into(),
));
}
let y_matrix = Array2::from_shape_vec((n_t, n_units), y_mat)
.map_err(|e| GreenersError::ShapeMismatch(e.to_string()))?;
let t_pre = times.iter().filter(|&&t| t < t0).count();
let t_post = n_t - t_pre;
if t_pre < 2 {
return Err(GreenersError::InvalidOperation(format!(
"synth: apenas {t_pre} período(s) pré-tratamento (mínimo 2)"
)));
}
let donor_idxs: Vec<usize> = (0..n_units).filter(|&j| j != treated_j).collect();
let n_donors = donor_idxs.len();
if n_donors == 0 {
return Err(GreenersError::InvalidOperation(
"synth: sem doadores no pool de controle".into(),
));
}
let y1_pre: Array1<f64> = (0..t_pre)
.map(|t| y_matrix[[t, treated_j]])
.collect::<Vec<_>>()
.into();
let y0_pre: Array2<f64> = build_donor_matrix(&y_matrix, t_pre, &donor_idxs, 0);
let mut q = y0_pre.t().dot(&y0_pre);
let mut c = y0_pre.t().dot(&y1_pre);
if let Some(cov_cols) = covariate_cols {
for col_name in cov_cols {
let x_col = df.get(col_name)?.to_owned();
let (x1_mean, x1_std) = pre_mean_std(&x_col, &t_col, &unit_ids, treated_unit, t0);
let scale = x1_std.max(1e-10);
let x0_means: Vec<f64> = donor_idxs
.iter()
.map(|&j| {
let uid = &units[j];
let (m, _) = pre_mean_std(&x_col, &t_col, &unit_ids, uid, t0);
m
})
.collect();
for j in 0..n_donors {
for k in 0..n_donors {
q[[j, k]] += x0_means[j] * x0_means[k] / (scale * scale);
}
c[j] += x1_mean * x0_means[j] / (scale * scale);
}
}
}
let weights = simplex_qp(&q, &c);
let y0_all = build_donor_matrix(&y_matrix, n_t, &donor_idxs, 0);
let synthetic_series: Vec<f64> = y0_all.dot(&weights).to_vec();
let actual_series: Vec<f64> = (0..n_t).map(|t| y_matrix[[t, treated_j]]).collect();
let rmspe_pre = {
let sse: f64 = (0..t_pre)
.map(|t| (actual_series[t] - synthetic_series[t]).powi(2))
.sum();
(sse / t_pre as f64).sqrt()
};
let rmspe_post = if t_post > 0 {
let sse: f64 = (t_pre..n_t)
.map(|t| (actual_series[t] - synthetic_series[t]).powi(2))
.sum();
Some((sse / t_post as f64).sqrt())
} else {
None
};
let donor_weights: Vec<(String, f64)> = donor_idxs
.iter()
.enumerate()
.map(|(i, &j)| (units[j].clone(), weights[i]))
.collect();
Ok(SynthResult {
weights: donor_weights,
synthetic_series,
actual_series,
time_index: times,
t0,
rmspe_pre,
rmspe_post,
treated_unit: treated_unit.to_string(),
outcome_name: outcome_col.to_string(),
n_donors,
t_pre,
t_post,
})
}
}
fn float_key(v: f64) -> String {
format!("{}", v.to_bits())
}
fn unit_ids_as_strings(df: &DataFrame, id_col: &str) -> Result<Vec<String>, GreenersError> {
if let Ok(arr) = df.get_string(id_col) {
return Ok(arr.to_vec());
}
if let Ok(arr) = df.get_int(id_col) {
return Ok(arr.iter().map(|v| v.to_string()).collect());
}
if let Ok(arr) = df.get(id_col) {
return Ok(arr.iter().map(|v| (*v as i64).to_string()).collect());
}
Err(GreenersError::VariableNotFound(id_col.to_string()))
}
fn build_donor_matrix(
y: &Array2<f64>,
n_rows: usize,
donors: &[usize],
_offset: usize,
) -> Array2<f64> {
let n_donors = donors.len();
let mut out = Array2::<f64>::zeros((n_rows, n_donors));
for (j, &dj) in donors.iter().enumerate() {
for t in 0..n_rows {
out[[t, j]] = y[[t, dj]];
}
}
out
}
fn pre_mean_std(
x: &Array1<f64>,
t_col: &Array1<f64>,
unit_ids: &[String],
target_unit: &str,
t0: f64,
) -> (f64, f64) {
let vals: Vec<f64> = x
.iter()
.enumerate()
.filter(|(i, _)| unit_ids[*i] == target_unit && t_col[*i] < t0)
.map(|(_, &v)| v)
.collect();
if vals.is_empty() {
return (0.0, 1.0);
}
let mean = vals.iter().sum::<f64>() / vals.len() as f64;
let var = vals.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / vals.len() as f64;
(mean, var.sqrt())
}
fn simplex_qp(q: &Array2<f64>, c: &Array1<f64>) -> Array1<f64> {
let j = c.len();
if j == 0 {
return Array1::zeros(0);
}
let mut w: Vec<f64> = vec![1.0 / j as f64; j];
let trace = q.diag().iter().sum::<f64>();
let lr = if trace > 1e-15 { 1.0 / trace } else { 1e-4 };
for _ in 0..20_000 {
let w_arr = Array1::from_vec(w.clone());
let grad = q.dot(&w_arr) - c;
let mut w_new: Vec<f64> = w
.iter()
.zip(grad.iter())
.map(|(&wi, &gi)| wi - lr * gi)
.collect();
project_simplex(&mut w_new);
let diff: f64 = w_new
.iter()
.zip(w.iter())
.map(|(a, b)| (a - b).powi(2))
.sum();
w = w_new;
if diff < 1e-14 {
break;
}
}
Array1::from_vec(w)
}
fn project_simplex(v: &mut [f64]) {
let mut u = v.to_vec();
u.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let mut cssv = 0.0_f64;
let mut rho = 0usize;
for (i, &ui) in u.iter().enumerate() {
cssv += ui;
if ui - (cssv - 1.0) / (i + 1) as f64 > 0.0 {
rho = i;
}
}
let theta = (u[..=rho].iter().sum::<f64>() - 1.0) / (rho + 1) as f64;
for vi in v.iter_mut() {
*vi = (*vi - theta).max(0.0);
}
}