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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
//! Spatial econometrics: SAR (Spatial Autoregressive) and SEM (Spatial Error Model).
//!
//! SAR:  y = ρWy + Xβ + ε
//! SEM:  y = Xβ + u,  u = λWu + ε
//!
//! where W is a row-standardized spatial weights matrix.

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

/// Result of spatial econometric estimation.
#[derive(Debug)]
pub struct SpatialResult {
    /// Model type: "sar" or "sem"
    pub model_type: String,
    /// Coefficients (spatial parameter first for SAR, then beta; for SEM just beta)
    pub params: Array1<f64>,
    /// Standard errors
    pub std_errors: Array1<f64>,
    /// t-statistics
    pub t_values: Array1<f64>,
    /// p-values
    pub p_values: Array1<f64>,
    /// Spatial parameter (rho for SAR, lambda for SEM)
    pub spatial_param: f64,
    /// Standard error of spatial parameter
    pub spatial_se: f64,
    /// t-stat of spatial parameter
    pub spatial_t: f64,
    /// p-value of spatial parameter
    pub spatial_p: f64,
    /// Beta coefficients (X effects)
    pub beta: Array1<f64>,
    /// R-squared
    pub r_squared: f64,
    /// Number of observations
    pub n_obs: usize,
    /// Log-likelihood
    pub log_likelihood: f64,
    /// Variable names
    pub variable_names: Option<Vec<String>>,
    /// Converged
    pub converged: bool,
}

impl fmt::Display for SpatialResult {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let title = if self.model_type == "sar" {
            " Spatial Autoregressive (SAR) "
        } else {
            " Spatial Error Model (SEM) "
        };
        writeln!(f, "\n{:=^78}", title)?;
        writeln!(f, "{:<20} {:>12}", "Observations:", self.n_obs)?;
        writeln!(f, "{:<20} {:>12.6}", "R-squared:", self.r_squared)?;
        writeln!(f, "{:<20} {:>12.6}", "Log-likelihood:", self.log_likelihood)?;

        let spatial_name = if self.model_type == "sar" {
            "rho (spatial lag)"
        } else {
            "lambda (spatial error)"
        };
        writeln!(f, "\n{:-^78}", "")?;
        writeln!(
            f,
            "{:<20} {:>12.6} {:>12.6} {:>10.3} {:>10.4}",
            spatial_name, self.spatial_param, self.spatial_se, self.spatial_t, self.spatial_p
        )?;

        writeln!(f, "\n{:-^78}", "")?;
        let header = format!(
            "{:<12} {:>12} {:>12} {:>10} {:>10}",
            "Variable", "Coef.", "Std.Err.", "t", "P>|t|"
        );
        writeln!(f, "{header}")?;
        writeln!(f, "{:-^78}", "")?;

        for i in 0..self.beta.len() {
            let name = self
                .variable_names
                .as_ref()
                .and_then(|n| n.get(i).cloned())
                .unwrap_or_else(|| format!("x{}", i));
            writeln!(
                f,
                "{:<12} {:>12.6} {:>12.6} {:>10.3} {:>10.4}",
                name,
                self.beta[i],
                self.std_errors[i + 1],
                self.t_values[i + 1],
                self.p_values[i + 1]
            )?;
        }
        write!(f, "{:=^78}", "")
    }
}

pub struct Spatial;

impl Spatial {
    /// Estimate SAR (Spatial Autoregressive) model: y = ρWy + Xβ + ε
    ///
    /// Uses maximum likelihood estimation. The spatial parameter ρ is
    /// estimated via grid search + golden section, then β is estimated
    /// via GLS.
    ///
    /// # Arguments
    /// * `y` - Dependent variable (n)
    /// * `x` - Independent variables (n × k, includes intercept if desired)
    /// * `w` - Row-standardized spatial weights matrix (n × n)
    /// * `variable_names` - Optional names for X variables
    pub fn fit_sar(
        y: &Array1<f64>,
        x: &Array2<f64>,
        w: &Array2<f64>,
        variable_names: Option<Vec<String>>,
    ) -> Result<SpatialResult, GreenersError> {
        let n = y.len();
        if x.nrows() != n || w.nrows() != n || w.ncols() != n {
            return Err(GreenersError::ShapeMismatch(
                "SAR: dimension mismatch between y, x, and W".into(),
            ));
        }

        // Grid search for rho over [-0.99, 0.99]
        let mut best_rho = 0.0_f64;
        let mut best_ll = f64::NEG_INFINITY;

        // Coarse grid
        let n_grid = 199;
        for i in 0..n_grid {
            let rho = -0.99 + 1.98 * i as f64 / (n_grid - 1) as f64;
            let ll = Self::sar_log_likelihood(y, x, w, rho)?;
            if ll > best_ll {
                best_ll = ll;
                best_rho = rho;
            }
        }

        // Fine search (golden section around best)
        let lo = best_rho - 0.05;
        let hi = best_rho + 0.05;
        let golden = 0.6180339887498949;
        let mut a = lo;
        let mut b = hi;
        let mut c = b - golden * (b - a);
        let mut d = a + golden * (b - a);
        let mut fc = Self::sar_log_likelihood(y, x, w, c)?;
        let mut fd = Self::sar_log_likelihood(y, x, w, d)?;
        for _ in 0..50 {
            if fc > fd {
                b = d;
                d = c;
                fd = fc;
                c = b - golden * (b - a);
                fc = Self::sar_log_likelihood(y, x, w, c)?;
            } else {
                a = c;
                c = d;
                fc = fd;
                d = a + golden * (b - a);
                fd = Self::sar_log_likelihood(y, x, w, d)?;
            }
        }
        best_rho = if fc > fd { c } else { d };
        best_ll = if fc > fd { fc } else { fd };

        // Compute beta at optimal rho
        let wy = w.dot(y);
        let y_star = y.clone() - best_rho * &wy;
        let x_star = x.clone(); // X is not transformed in SAR
        let xt = x_star.t();
        let xtx = xt.dot(&x_star);
        let xtx_inv = xtx.inv()?;
        let xty = xt.dot(&y_star);
        let beta: Array1<f64> = xtx_inv.dot(&xty);

        // Residuals and sigma2
        let fitted = &x_star.dot(&beta) + best_rho * &wy;
        let residuals = y - &fitted;
        let sigma2 = residuals.dot(&residuals) / n as f64;

        // Standard errors (simplified: treat rho as known)
        let cov_beta = xtx_inv * sigma2;
        let beta_se = cov_beta.diag().mapv(|v| v.sqrt());

        // SE for rho (from Hessian approximation)
        let rho_se = {
            let h = 0.01;
            let ll_p = Self::sar_log_likelihood(y, x, w, best_rho + h)?;
            let ll_m = Self::sar_log_likelihood(y, x, w, best_rho - h)?;
            let second_deriv = (ll_p - 2.0 * best_ll + ll_m) / (h * h);
            if second_deriv < 0.0 {
                (-1.0 / second_deriv).sqrt()
            } else {
                f64::NAN
            }
        };

        let normal =
            Normal::new(0.0, 1.0).map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;

        let rho_t = best_rho / rho_se;
        let rho_p = if rho_t.is_nan() || rho_t.is_infinite() {
            f64::NAN
        } else {
            2.0 * (1.0 - normal.cdf(rho_t.abs()))
        };

        let beta_t = &beta / &beta_se;
        let beta_p = beta_t.mapv(|t| {
            if t.is_nan() || t.is_infinite() {
                f64::NAN
            } else {
                2.0 * (1.0 - normal.cdf(t.abs()))
            }
        });

        // Combine params: [rho, beta...]
        let mut params = vec![best_rho];
        params.extend(beta.iter().cloned());
        let mut se = vec![rho_se];
        se.extend(beta_se.iter().cloned());
        let mut t_vals = vec![rho_t];
        t_vals.extend(beta_t.iter().cloned());
        let mut p_vals = vec![rho_p];
        p_vals.extend(beta_p.iter().cloned());

        let y_mean = y.mean().unwrap_or(0.0);
        let tss = y.mapv(|v| (v - y_mean).powi(2)).sum();
        let rss = residuals.dot(&residuals);
        let r_squared = if tss > 1e-15 { 1.0 - rss / tss } else { 0.0 };

        Ok(SpatialResult {
            model_type: "sar".into(),
            params: Array1::from(params),
            std_errors: Array1::from(se),
            t_values: Array1::from(t_vals),
            p_values: Array1::from(p_vals),
            spatial_param: best_rho,
            spatial_se: rho_se,
            spatial_t: rho_t,
            spatial_p: rho_p,
            beta,
            r_squared,
            n_obs: n,
            log_likelihood: best_ll,
            variable_names,
            converged: true,
        })
    }

    /// Estimate SEM (Spatial Error Model): y = Xβ + u, u = λWu + ε
    pub fn fit_sem(
        y: &Array1<f64>,
        x: &Array2<f64>,
        w: &Array2<f64>,
        variable_names: Option<Vec<String>>,
    ) -> Result<SpatialResult, GreenersError> {
        let n = y.len();
        if x.nrows() != n || w.nrows() != n || w.ncols() != n {
            return Err(GreenersError::ShapeMismatch(
                "SEM: dimension mismatch between y, x, and W".into(),
            ));
        }

        // OLS first to get initial beta
        let xt = x.t();
        let xtx = xt.dot(x);
        let xtx_inv = xtx.inv()?;
        let xty = xt.dot(y);
        let beta_ols = xtx_inv.dot(&xty);

        // Grid search for lambda
        let mut best_lambda = 0.0_f64;
        let mut best_ll = f64::NEG_INFINITY;

        let n_grid = 199;
        for i in 0..n_grid {
            let lambda = -0.99 + 1.98 * i as f64 / (n_grid - 1) as f64;
            let ll = Self::sem_log_likelihood(y, x, w, lambda, &beta_ols)?;
            if ll > best_ll {
                best_ll = ll;
                best_lambda = lambda;
            }
        }

        // Golden section refinement
        let golden = 0.6180339887498949;
        let mut a = best_lambda - 0.05;
        let mut b = best_lambda + 0.05;
        let mut c = b - golden * (b - a);
        let mut d = a + golden * (b - a);
        let mut fc = Self::sem_log_likelihood(y, x, w, c, &beta_ols)?;
        let mut fd = Self::sem_log_likelihood(y, x, w, d, &beta_ols)?;
        for _ in 0..50 {
            if fc > fd {
                b = d;
                d = c;
                fd = fc;
                c = b - golden * (b - a);
                fc = Self::sem_log_likelihood(y, x, w, c, &beta_ols)?;
            } else {
                a = c;
                c = d;
                fc = fd;
                d = a + golden * (b - a);
                fd = Self::sem_log_likelihood(y, x, w, d, &beta_ols)?;
            }
        }
        best_lambda = if fc > fd { c } else { d };
        best_ll = if fc > fd { fc } else { fd };

        // Re-estimate beta with FGLS: beta = (X'(I-λW)'(I-λW)X)^{-1} X'(I-λW)'(I-λW)y
        let i_minus_lw = Array2::eye(n) - best_lambda * w;
        let x_transformed = i_minus_lw.dot(x);
        let y_transformed = i_minus_lw.dot(y);
        let xt_t = x_transformed.t();
        let xtx_t = xt_t.dot(&x_transformed);
        let xtx_t_inv = xtx_t.inv()?;
        let xty_t = xt_t.dot(&y_transformed);
        let beta: Array1<f64> = xtx_t_inv.dot(&xty_t);

        // Residuals
        let residuals = y - x.dot(&beta);
        let sigma2 = residuals.dot(&residuals) / n as f64;

        let cov_beta = xtx_t_inv * sigma2;
        let beta_se = cov_beta.diag().mapv(|v| v.sqrt());

        let lambda_se = {
            let h = 0.01;
            let ll_p = Self::sem_log_likelihood(y, x, w, best_lambda + h, &beta)?;
            let ll_m = Self::sem_log_likelihood(y, x, w, best_lambda - h, &beta)?;
            let second_deriv = (ll_p - 2.0 * best_ll + ll_m) / (h * h);
            if second_deriv < 0.0 {
                (-1.0 / second_deriv).sqrt()
            } else {
                f64::NAN
            }
        };

        let normal =
            Normal::new(0.0, 1.0).map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;

        let lambda_t = best_lambda / lambda_se;
        let lambda_p = if lambda_t.is_nan() || lambda_t.is_infinite() {
            f64::NAN
        } else {
            2.0 * (1.0 - normal.cdf(lambda_t.abs()))
        };

        let beta_t = &beta / &beta_se;
        let beta_p = beta_t.mapv(|t| {
            if t.is_nan() || t.is_infinite() {
                f64::NAN
            } else {
                2.0 * (1.0 - normal.cdf(t.abs()))
            }
        });

        let mut params = vec![best_lambda];
        params.extend(beta.iter().cloned());
        let mut se = vec![lambda_se];
        se.extend(beta_se.iter().cloned());
        let mut t_vals = vec![lambda_t];
        t_vals.extend(beta_t.iter().cloned());
        let mut p_vals = vec![lambda_p];
        p_vals.extend(beta_p.iter().cloned());

        let y_mean = y.mean().unwrap_or(0.0);
        let tss = y.mapv(|v| (v - y_mean).powi(2)).sum();
        let rss = residuals.dot(&residuals);
        let r_squared = if tss > 1e-15 { 1.0 - rss / tss } else { 0.0 };

        Ok(SpatialResult {
            model_type: "sem".into(),
            params: Array1::from(params),
            std_errors: Array1::from(se),
            t_values: Array1::from(t_vals),
            p_values: Array1::from(p_vals),
            spatial_param: best_lambda,
            spatial_se: lambda_se,
            spatial_t: lambda_t,
            spatial_p: lambda_p,
            beta,
            r_squared,
            n_obs: n,
            log_likelihood: best_ll,
            variable_names,
            converged: true,
        })
    }

    /// SAR log-likelihood for a given rho
    fn sar_log_likelihood(
        y: &Array1<f64>,
        x: &Array2<f64>,
        w: &Array2<f64>,
        rho: f64,
    ) -> Result<f64, GreenersError> {
        let n = y.len();
        let wy = w.dot(y);
        let y_star = y.clone() - rho * &wy;

        let xt = x.t();
        let xtx = xt.dot(x);
        let xtx_inv = xtx.inv()?;
        let xty = xt.dot(&y_star);
        let beta: Array1<f64> = xtx_inv.dot(&xty);

        let residuals = &y_star - x.dot(&beta);
        let rss = residuals.dot(&residuals);
        let sigma2 = rss / n as f64;

        // Log-det(I - rho*W) via eigenvalues approximation
        // For simplicity, use the trace approximation: log|I-ρW| ≈ -ρ*tr(W) - ρ²/2*tr(W²) - ...
        // For row-standardized W, tr(W) ≈ 0, so this is small.
        // We use the exact approach: compute eigenvalues of W (expensive but correct for small n)
        // For now, use a simplified Jacobian: log|I-ρW| ≈ n*log(1-ρ²*mean_eig²)
        // This is an approximation; exact computation requires eigenvalue decomposition.
        let log_det = -(n as f64) * (1.0 - rho * rho).max(1e-10).ln() / 2.0;

        let ll = log_det
            - n as f64 / 2.0 * (2.0 * std::f64::consts::PI * sigma2).ln()
            - rss / (2.0 * sigma2);

        Ok(ll)
    }

    /// SEM log-likelihood for a given lambda
    fn sem_log_likelihood(
        y: &Array1<f64>,
        x: &Array2<f64>,
        w: &Array2<f64>,
        lambda: f64,
        beta: &Array1<f64>,
    ) -> Result<f64, GreenersError> {
        let n = y.len();
        let residuals = y.clone() - x.dot(beta);
        let wu = w.dot(&residuals);
        let u_star = &residuals - lambda * &wu;
        let rss = u_star.dot(&u_star);
        let sigma2 = rss / n as f64;

        let log_det = -(n as f64) * (1.0 - lambda * lambda).max(1e-10).ln() / 2.0;

        let ll = log_det
            - n as f64 / 2.0 * (2.0 * std::f64::consts::PI * sigma2).ln()
            - rss / (2.0 * sigma2);

        Ok(ll)
    }
}