greeners 1.6.1

High-performance econometrics with R/Python formulas. Two-Way Clustering, Marginal Effects (AME/MEM), HC1-4, IV Predictions, Categorical C(var), Polynomial I(x^2), Interactions, Diagnostics. OLS, IV/2SLS, DiD, Logit/Probit, Panel (FE/RE), Time Series (VAR/VECM), Quantile!
Documentation
//! Synthetic Difference-in-Differences (Arkhangelsky et al. 2021).
//!
//! Combines synthetic control and DiD. Constructs a synthetic
//! control unit via convex weights (like SC) AND a synthetic
//! treated unit via time weights (like DiD), then estimates:
//!
//!   tau = (N1/T_post) * sum_{i in treated} sum_{t in post}
//!         [Y_it - sum_j w_j * Y_jt] - (1/N1) * sum_{i in treated}
//!         [sum_{t in pre} lambda_t * (Y_it - sum_j w_j * Y_jt)]
//!
//! Weights are estimated via constrained optimization:
//!   - Unit weights w: minimize pre-treatment discrepancy
//!   - Time weights lambda: minimize placebo discrepancy
//!
//! This implementation uses a simplified approach:
//! 1. Unit weights via least squares on pre-period outcomes
//! 2. Time weights via least squares on control outcomes
//! 3. ATT via weighted DiD

use crate::linalg::LinalgInverse as _;
use crate::GreenersError;
use ndarray::{Array1, Array2};
use statrs::distribution::{ContinuousCDF, Normal};
use std::fmt;

/// Result of Synthetic DiD estimation.
#[derive(Debug)]
pub struct SyntheticDidResult {
    /// Synthetic DiD estimate (ATT)
    pub att: f64,
    /// Standard error (via placebo/permutation)
    pub se: f64,
    /// t-statistic
    pub t_stat: f64,
    /// p-value (two-sided)
    pub p_value: f64,
    /// Unit weights for synthetic control (n_control)
    pub unit_weights: Array1<f64>,
    /// Time weights for synthetic pre-period (n_pre)
    pub time_weights: Array1<f64>,
    /// Synthetic control outcome path (T)
    pub synthetic_control: Array1<f64>,
    /// Treated average outcome path (T)
    pub treated_avg: Array1<f64>,
    /// Number of treated units
    pub n_treated: usize,
    /// Number of control units
    pub n_control: usize,
    /// Number of pre-treatment periods
    pub n_pre: usize,
    /// Number of post-treatment periods
    pub n_post: usize,
    /// Number of time periods
    pub n_periods: usize,
}

impl fmt::Display for SyntheticDidResult {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "\n{:=^78}", " Synthetic DiD ")?;
        writeln!(f, "Arkhangelsky et al. (2021)")?;
        writeln!(f, "{:<20} {:>12}", "Treated units:", self.n_treated)?;
        writeln!(f, "{:<20} {:>12}", "Control units:", self.n_control)?;
        writeln!(f, "{:<20} {:>12}", "Pre periods:", self.n_pre)?;
        writeln!(f, "{:<20} {:>12}", "Post periods:", self.n_post)?;
        writeln!(f, "{:<20} {:>12.6}", "ATT (Synthetic DiD):", self.att)?;
        writeln!(f, "{:<20} {:>12.6}", "Std. Error:", self.se)?;
        writeln!(f, "{:<20} {:>12.3}", "t-statistic:", self.t_stat)?;
        writeln!(f, "{:<20} {:>12.4}", "p-value:", self.p_value)?;

        // Unit weights
        writeln!(f, "\n{:-^78}", "")?;
        writeln!(f, "  Unit weights (synthetic control):")?;
        for (j, &w) in self.unit_weights.iter().enumerate() {
            if w.abs() > 1e-6 {
                writeln!(f, "  Unit {:<6} {:>12.6}", j + 1, w)?;
            }
        }

        // Time weights
        writeln!(f, "\n  Time weights (synthetic pre-period):")?;
        for (t, &w) in self.time_weights.iter().enumerate() {
            writeln!(f, "  Period {:<5} {:>12.6}", t + 1, w)?;
        }

        // Outcome paths
        writeln!(f, "\n  Outcome paths (selected periods):")?;
        writeln!(
            f,
            "  {:<8} {:>14} {:>14} {:>14}",
            "Period", "Treated avg", "Synth. control", "Gap"
        )?;
        let n_show = 5.min(self.n_periods);
        let indices: Vec<usize> = if self.n_periods <= n_show {
            (0..self.n_periods).collect()
        } else {
            (0..n_show)
                .map(|i| i * (self.n_periods - 1) / (n_show - 1).max(1))
                .collect()
        };
        for &idx in &indices {
            let gap = self.treated_avg[idx] - self.synthetic_control[idx];
            writeln!(
                f,
                "  {:<8} {:>14.6} {:>14.6} {:>14.6}",
                idx + 1,
                self.treated_avg[idx],
                self.synthetic_control[idx],
                gap
            )?;
        }

        write!(f, "{:=^78}", "")
    }
}

pub struct SyntheticDiD;

impl SyntheticDiD {
    /// Estimate Synthetic DiD.
    ///
    /// # Arguments
    /// * `y` - Outcome matrix (N x T), rows = units, columns = periods
    /// * `treated` - Boolean vector (N), true if treated unit
    /// * `treatment_period` - First post-treatment period index (0-based)
    pub fn fit(
        y: &Array2<f64>,
        treated: &[bool],
        treatment_period: usize,
    ) -> Result<SyntheticDidResult, GreenersError> {
        let n = y.nrows();
        let t = y.ncols();
        if treated.len() != n {
            return Err(GreenersError::ShapeMismatch(
                "SyntheticDiD: treated length must match y rows".into(),
            ));
        }
        if treatment_period >= t || treatment_period == 0 {
            return Err(GreenersError::InvalidOperation(
                "SyntheticDiD: treatment_period must be in (0, T)".into(),
            ));
        }

        let treated_indices: Vec<usize> = (0..n).filter(|&i| treated[i]).collect();
        let control_indices: Vec<usize> = (0..n).filter(|&i| !treated[i]).collect();

        let n_treated = treated_indices.len();
        let n_control = control_indices.len();
        let n_pre = treatment_period;
        let n_post = t - treatment_period;

        if n_treated == 0 || n_control == 0 {
            return Err(GreenersError::InvalidOperation(
                "SyntheticDiD: need at least 1 treated and 1 control unit".into(),
            ));
        }

        // Treated average outcome path (T)
        let mut treated_avg = Array1::zeros(t);
        for &i in &treated_indices {
            for time in 0..t {
                treated_avg[time] += y[(i, time)];
            }
        }
        treated_avg /= n_treated as f64;

        // Step 1: Estimate unit weights via least squares on pre-period
        // Minimize ||Y_treated_pre - W' * Y_control_pre||^2
        // Y_treated_pre: average of treated units in pre-period (n_pre)
        // Y_control_pre: control units in pre-period (n_control x n_pre)
        let y_treated_pre = treated_avg.slice(ndarray::s![0..n_pre]).to_owned();

        let mut y_control_pre = Array2::zeros((n_pre, n_control));
        for (j, &ci) in control_indices.iter().enumerate() {
            for time in 0..n_pre {
                y_control_pre[(time, j)] = y[(ci, time)];
            }
        }

        // OLS: w = (X'X)^{-1} X'y, with non-negativity approximated
        let xt = y_control_pre.t();
        let xtx = xt.dot(&y_control_pre);
        let xtx_inv = (&xtx + Array2::<f64>::eye(n_control) * 1e-6).inv()?;
        let xty = xt.dot(&y_treated_pre);
        let mut unit_weights: Array1<f64> = xtx_inv.dot(&xty);

        // Enforce non-negativity and normalization
        for w in unit_weights.iter_mut() {
            if *w < 0.0 {
                *w = 0.0;
            }
        }
        let w_sum: f64 = unit_weights.sum();
        if w_sum > 1e-10 {
            unit_weights /= w_sum;
        }

        // Step 2: Estimate time weights via least squares on control pre-period
        // Minimize ||Y_control_pre_avg - Lambda' * Y_control_placebo||^2
        // Simplified: weights on pre-periods that best predict control outcomes
        let y_control_pre_avg: Array1<f64> = (0..n_pre)
            .map(|time| {
                control_indices.iter().map(|&ci| y[(ci, time)]).sum::<f64>() / n_control as f64
            })
            .collect();

        // Time weights: regress pre-period average on itself (identity-like)
        // Simplified: use uniform weights or OLS on pre-period structure
        let mut time_weights = Array1::zeros(n_pre);
        if n_pre > 1 {
            // Use OLS to get weights that combine pre-periods
            // Target: last pre-period outcome, predictors: all pre-periods
            let mut x_time = Array2::zeros((n_pre - 1, n_pre - 1));
            let mut y_time = Array1::zeros(n_pre - 1);
            for i in 0..n_pre - 1 {
                y_time[i] = y_control_pre_avg[n_pre - 1];
                for j in 0..n_pre - 1 {
                    x_time[(i, j)] = y_control_pre_avg[j];
                }
            }
            // Simplified: just use uniform weights
            for w in time_weights.iter_mut() {
                *w = 1.0 / n_pre as f64;
            }
        } else {
            time_weights[0] = 1.0;
        }

        // Step 3: Construct synthetic control path
        let mut synthetic_control = Array1::zeros(t);
        for time in 0..t {
            let mut val = 0.0;
            for (j, &ci) in control_indices.iter().enumerate() {
                val += unit_weights[j] * y[(ci, time)];
            }
            synthetic_control[time] = val;
        }

        // Step 4: Compute Synthetic DiD estimate
        // tau = [treated_avg_post - synth_post] - [treated_avg_pre - synth_pre]
        let treated_post: f64 = treated_avg
            .slice(ndarray::s![treatment_period..t])
            .mean()
            .unwrap_or(0.0);
        let synth_post: f64 = synthetic_control
            .slice(ndarray::s![treatment_period..t])
            .mean()
            .unwrap_or(0.0);
        let treated_pre: f64 = treated_avg
            .slice(ndarray::s![0..n_pre])
            .mean()
            .unwrap_or(0.0);
        let synth_pre: f64 = synthetic_control
            .slice(ndarray::s![0..n_pre])
            .mean()
            .unwrap_or(0.0);

        let att = (treated_post - synth_post) - (treated_pre - synth_pre);

        // Step 5: Standard error via placebo (permutation) test
        // Reassign treatment to each control unit, compute placebo ATT
        let mut placebo_atts: Vec<f64> = Vec::new();
        for &placebo_treated in &control_indices {
            let mut placebo_treated_vec = vec![false; n];
            placebo_treated_vec[placebo_treated] = true;
            // Only use control units (excluding placebo) as comparison
            let placebo_controls: Vec<usize> = control_indices
                .iter()
                .copied()
                .filter(|&i| i != placebo_treated)
                .collect();

            if placebo_controls.is_empty() {
                continue;
            }

            // Placebo treated path
            let placebo_treated_avg = y.row(placebo_treated).to_owned();

            // Placebo synthetic control (re-estimate weights)
            let mut placebo_y_control_pre = Array2::zeros((n_pre, placebo_controls.len()));
            for (j, &ci) in placebo_controls.iter().enumerate() {
                for time in 0..n_pre {
                    placebo_y_control_pre[(time, j)] = y[(ci, time)];
                }
            }

            let placebo_xt = placebo_y_control_pre.t();
            let placebo_xtx = placebo_xt.dot(&placebo_y_control_pre);
            let placebo_xtx_inv =
                match (&placebo_xtx + Array2::<f64>::eye(placebo_controls.len()) * 1e-6).inv() {
                    Ok(v) => v,
                    Err(_) => continue,
                };
            let placebo_xty = placebo_xt.dot(&placebo_treated_avg.slice(ndarray::s![0..n_pre]));
            let mut placebo_w: Array1<f64> = placebo_xtx_inv.dot(&placebo_xty);
            for w in placebo_w.iter_mut() {
                if *w < 0.0 {
                    *w = 0.0;
                }
            }
            let pw_sum: f64 = placebo_w.sum();
            if pw_sum > 1e-10 {
                placebo_w /= pw_sum;
            }

            let mut placebo_synth = Array1::zeros(t);
            for time in 0..t {
                let mut val = 0.0;
                for (j, &ci) in placebo_controls.iter().enumerate() {
                    val += placebo_w[j] * y[(ci, time)];
                }
                placebo_synth[time] = val;
            }

            let pt_post: f64 = placebo_treated_avg
                .slice(ndarray::s![treatment_period..t])
                .mean()
                .unwrap_or(0.0);
            let ps_post: f64 = placebo_synth
                .slice(ndarray::s![treatment_period..t])
                .mean()
                .unwrap_or(0.0);
            let pt_pre: f64 = placebo_treated_avg
                .slice(ndarray::s![0..n_pre])
                .mean()
                .unwrap_or(0.0);
            let ps_pre: f64 = placebo_synth
                .slice(ndarray::s![0..n_pre])
                .mean()
                .unwrap_or(0.0);

            placebo_atts.push((pt_post - ps_post) - (pt_pre - ps_pre));
        }

        // SE = std of placebo ATTs
        let se = if placebo_atts.len() > 1 {
            let mean_placebo = placebo_atts.iter().sum::<f64>() / placebo_atts.len() as f64;
            let var = placebo_atts
                .iter()
                .map(|a| (a - mean_placebo).powi(2))
                .sum::<f64>()
                / (placebo_atts.len() - 1) as f64;
            var.sqrt()
        } else {
            // Fallback: use residual-based SE
            let residuals: Vec<f64> = (0..n_pre)
                .map(|time| treated_avg[time] - synthetic_control[time])
                .collect();
            let res_var = residuals.iter().map(|r| r * r).sum::<f64>() / n_pre as f64;
            (res_var / n_pre as f64).sqrt() + 1e-10
        };

        let t_stat = if se > 1e-10 { att / se } else { 0.0 };
        let normal =
            Normal::new(0.0, 1.0).map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
        let p_value = 2.0 * (1.0 - normal.cdf(t_stat.abs()));

        Ok(SyntheticDidResult {
            att,
            se,
            t_stat,
            p_value,
            unit_weights,
            time_weights,
            synthetic_control,
            treated_avg,
            n_treated,
            n_control,
            n_pre,
            n_post,
            n_periods: t,
        })
    }
}