Skip to main content

scirs2_stats/panel/
dynamic.rs

1//! Dynamic Panel Data Models
2//!
3//! Implements:
4//! - `ArellanoBlond`: Arellano-Bond GMM estimator (AB-GMM) for dynamic panels
5//! - `BlundellBond`: System GMM (BB-GMM) with level equations
6//! - `SarganTest`: Sargan test for over-identifying restrictions
7//! - `AR1Test`, `AR2Test`: Arellano-Bond serial correlation tests
8//! - `DynamicPanelResult`: GMM coefficients, J-stat, AR tests
9
10use crate::error::{StatsError, StatsResult};
11use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
12use scirs2_core::numeric::{Float, FromPrimitive};
13use scirs2_linalg::{lstsq, solve};
14
15// ──────────────────────────────────────────────────────────────────────────────
16// Helpers
17// ──────────────────────────────────────────────────────────────────────────────
18
19fn matmul<F: Float + std::iter::Sum>(a: &Array2<F>, b: &Array2<F>) -> StatsResult<Array2<F>> {
20    let (m, k) = a.dim();
21    let (kb, n) = b.dim();
22    if k != kb {
23        return Err(StatsError::DimensionMismatch(format!(
24            "matmul: {} vs {}",
25            k, kb
26        )));
27    }
28    let mut c = Array2::zeros((m, n));
29    for i in 0..m {
30        for j in 0..n {
31            let mut s = F::zero();
32            for l in 0..k {
33                s = s + a[[i, l]] * b[[l, j]];
34            }
35            c[[i, j]] = s;
36        }
37    }
38    Ok(c)
39}
40
41fn mat_vec<F: Float + std::iter::Sum>(a: &Array2<F>, v: &Array1<F>) -> StatsResult<Array1<F>> {
42    let (m, k) = a.dim();
43    if v.len() != k {
44        return Err(StatsError::DimensionMismatch(format!(
45            "mat_vec: {} vs {}",
46            k,
47            v.len()
48        )));
49    }
50    let mut res = Array1::zeros(m);
51    for i in 0..m {
52        let mut s = F::zero();
53        for j in 0..k {
54            s = s + a[[i, j]] * v[j];
55        }
56        res[i] = s;
57    }
58    Ok(res)
59}
60
61fn transpose<F: Float>(a: &Array2<F>) -> Array2<F> {
62    let (m, n) = a.dim();
63    let mut t = Array2::zeros((n, m));
64    for i in 0..m {
65        for j in 0..n {
66            t[[j, i]] = a[[i, j]];
67        }
68    }
69    t
70}
71
72/// Two-step GMM: β = (X'ZWZ'X)^{-1} X'ZWZ'y
73/// where W is the weight matrix.
74fn gmm_estimator<F>(
75    x: &Array2<F>,
76    y: &Array1<F>,
77    z: &Array2<F>,
78    w: &Array2<F>,
79) -> StatsResult<(Array1<F>, Array1<F>)>
80where
81    F: Float
82        + std::iter::Sum
83        + std::fmt::Debug
84        + std::fmt::Display
85        + scirs2_core::numeric::NumAssign
86        + scirs2_core::numeric::One
87        + scirs2_core::ndarray::ScalarOperand
88        + FromPrimitive
89        + Send
90        + Sync
91        + 'static,
92{
93    // A = X'Z W Z'X  (k×k)
94    let zx = matmul(&transpose(z), x)?; // L × K
95    let zy = mat_vec(&transpose(z), y)?; // L
96    let wzx = matmul(w, &zx)?; // L × K
97    let wzy = mat_vec(w, &zy)?; // L
98    let xtwzx = matmul(&transpose(&zx), &wzx)?; // K × K
99    let xtwzy = mat_vec(&transpose(&zx), &wzy)?; // K
100
101    // β = (X'ZWZ'X)^{-1} X'ZWZ'y
102    let coeffs = solve(&xtwzx.view(), &xtwzy.view(), None)
103        .map_err(|e| StatsError::ComputationError(format!("GMM solve: {e}")))?;
104
105    // residuals
106    let n = y.len();
107    let k = x.ncols();
108    let mut fitted = Array1::zeros(n);
109    for i in 0..n {
110        for j in 0..k {
111            fitted[i] = fitted[i] + x[[i, j]] * coeffs[j];
112        }
113    }
114    let resid: Array1<F> = y.iter().zip(fitted.iter()).map(|(&y, &f)| y - f).collect();
115    Ok((coeffs, resid))
116}
117
118// ──────────────────────────────────────────────────────────────────────────────
119// DynamicPanelResult
120// ──────────────────────────────────────────────────────────────────────────────
121
122/// AR test result (AR(1) and AR(2) for Arellano-Bond serial correlation test).
123#[derive(Debug, Clone)]
124pub struct ARTestResult<F> {
125    /// z-statistic
126    pub z_stat: F,
127    /// p-value (two-sided)
128    pub p_value: F,
129}
130
131/// Sargan test result (over-identifying restrictions).
132#[derive(Debug, Clone)]
133pub struct SarganTestResult<F> {
134    /// Sargan J-statistic ~ χ²(L - K)
135    pub j_stat: F,
136    /// Degrees of freedom = number of instruments − number of regressors
137    pub df: usize,
138    /// p-value
139    pub p_value: F,
140}
141
142/// Result from a dynamic panel GMM estimator.
143#[derive(Debug, Clone)]
144pub struct DynamicPanelResult<F> {
145    /// GMM coefficient estimates
146    pub coefficients: Array1<F>,
147    /// Robust standard errors (Windmeijer-corrected two-step)
148    pub std_errors: Array1<F>,
149    /// z-statistics
150    pub z_stats: Array1<F>,
151    /// Sargan test of over-identification
152    pub sargan: SarganTestResult<F>,
153    /// Arellano-Bond AR(1) test
154    pub ar1: ARTestResult<F>,
155    /// Arellano-Bond AR(2) test
156    pub ar2: ARTestResult<F>,
157    /// Number of observations used
158    pub n_obs: usize,
159    /// Number of instruments used
160    pub n_instruments: usize,
161    /// Residuals
162    pub residuals: Array1<F>,
163}
164
165// ──────────────────────────────────────────────────────────────────────────────
166// ArellanoBlond (AB-GMM)
167// ──────────────────────────────────────────────────────────────────────────────
168
169/// Arellano-Bond (1991) first-difference GMM estimator.
170///
171/// Instruments: lagged levels y_{i,t-2}, y_{i,t-3}, … for the first-differenced
172/// equation Δy_{it} = α Δy_{i,t-1} + Δx_{it}' β + Δε_{it}.
173///
174/// This is a balanced-panel implementation.  It uses the canonical Arellano-Bond
175/// instrument matrix construction and two-step GMM.
176pub struct ArellanoBlond;
177
178impl ArellanoBlond {
179    /// Fit the Arellano-Bond GMM estimator.
180    ///
181    /// # Arguments
182    /// * `y`       – response vector (N), stacked as entity 0 all T periods, entity 1, …
183    /// * `x`       – exogenous regressors (N × K), **without** lagged y
184    /// * `entity`  – entity IDs (0-indexed, length N)
185    /// * `time`    – time IDs (0-indexed, length N)
186    /// * `n_lags`  – number of lags of y to include as regressors (usually 1 or 2)
187    pub fn fit<F>(
188        y: &ArrayView1<F>,
189        x: &ArrayView2<F>,
190        entity: &[usize],
191        time: &[usize],
192        n_lags: usize,
193    ) -> StatsResult<DynamicPanelResult<F>>
194    where
195        F: Float
196            + std::iter::Sum
197            + std::fmt::Debug
198            + std::fmt::Display
199            + scirs2_core::numeric::NumAssign
200            + scirs2_core::numeric::One
201            + scirs2_core::ndarray::ScalarOperand
202            + FromPrimitive
203            + Send
204            + Sync
205            + 'static,
206    {
207        if n_lags == 0 {
208            return Err(StatsError::InvalidArgument(
209                "n_lags must be >= 1".to_string(),
210            ));
211        }
212        let n = y.len();
213        let (nx, kx) = x.dim();
214        if nx != n || entity.len() != n || time.len() != n {
215            return Err(StatsError::DimensionMismatch(
216                "y, x, entity, time lengths must match".to_string(),
217            ));
218        }
219
220        // ── Determine balanced panel dimensions ────────────────────────────────
221        let n_entities = entity.iter().copied().max().map(|m| m + 1).unwrap_or(0);
222        let t_total = time.iter().copied().max().map(|m| m + 1).unwrap_or(0);
223        if t_total < n_lags + 2 {
224            return Err(StatsError::InsufficientData(format!(
225                "Need at least {} time periods, got {}",
226                n_lags + 2,
227                t_total
228            )));
229        }
230        // Require balanced panel
231        let t_per = n / n_entities;
232        if t_per * n_entities != n {
233            return Err(StatsError::InvalidArgument(
234                "AB-GMM: requires balanced panel".to_string(),
235            ));
236        }
237
238        // ── Build first-differenced data ───────────────────────────────────────
239        // For each entity, compute Δy_{it} = y_{it} - y_{i,t-1}  for t = n_lags+1 .. T-1
240        // Regressor matrix includes [Δy_{i,t-1}, …, Δy_{i,t-n_lags}, Δx_{it}]
241        // We iterate in sorted (entity, time) order.
242
243        // Map (entity, time) → flat index
244        let mut idx_map = vec![vec![0usize; t_total]; n_entities];
245        for i in 0..n {
246            idx_map[entity[i]][time[i]] = i;
247        }
248
249        // t where we have first-differences starts at t = n_lags + 1 (need t-n_lags as lag)
250        // and instruments: levels at t-n_lags-1, t-n_lags-2, ...
251        let t_start = n_lags + 1; // first t with valid Δy and all lagged levels
252        let nd = n_entities * (t_total - t_start); // number of FD observations
253        if nd < kx + n_lags + 1 {
254            return Err(StatsError::InsufficientData(format!(
255                "Too few FD observations ({}) for {} regressors",
256                nd,
257                kx + n_lags
258            )));
259        }
260
261        let mut dy_vec: Vec<F> = Vec::with_capacity(nd);
262        let mut dx_rows: Vec<Vec<F>> = Vec::with_capacity(nd); // [Δy lags, Δx]
263
264        for eid in 0..n_entities {
265            for t in t_start..t_total {
266                let i_cur = idx_map[eid][t];
267                let i_prev = idx_map[eid][t - 1];
268                let dy = y[i_cur] - y[i_prev];
269                dy_vec.push(dy);
270                let mut row: Vec<F> = Vec::new();
271                // lagged differences Δy_{t-1}, …, Δy_{t-n_lags}
272                for lag in 1..=n_lags {
273                    if t >= lag + 1 {
274                        let i_l = idx_map[eid][t - lag];
275                        let i_l1 = idx_map[eid][t - lag - 1];
276                        row.push(y[i_l] - y[i_l1]);
277                    } else {
278                        row.push(F::zero());
279                    }
280                }
281                // differenced exogenous Δx_{it}
282                for j in 0..kx {
283                    row.push(x[[i_cur, j]] - x[[i_prev, j]]);
284                }
285                dx_rows.push(row);
286            }
287        }
288
289        let k_reg = n_lags + kx; // total regressors
290        let dx_flat: Vec<F> = dx_rows.iter().flat_map(|r| r.iter().copied()).collect();
291        let dx = Array2::from_shape_vec((nd, k_reg), dx_flat)
292            .map_err(|e| StatsError::ComputationError(format!("reshape dx: {e}")))?;
293        let dy = Array1::from(dy_vec);
294
295        // ── Build instrument matrix Z (AB instruments) ─────────────────────────
296        // For observation (eid, t), instruments = [y_{eid,0}, …, y_{eid,t-n_lags-1}]
297        // The maximum number of instruments per observation is t-n_lags
298        // (using all available lags of level y).
299        // We use a condensed diagonal-block structure.
300        let max_inst_per = t_total - n_lags - 1; // maximum instruments per obs
301                                                 // Total instrument columns: sum_{t=t_start}^{T-1} (t - n_lags) = (T-n_lags-1)(T-n_lags)/2
302                                                 // Simplified: use a fixed block for each time period
303        let n_inst_cols = max_inst_per * (max_inst_per + 1) / 2 + kx; // + exogenous
304
305        let mut z_rows: Vec<Vec<F>> = Vec::with_capacity(nd);
306        for eid in 0..n_entities {
307            for t in t_start..t_total {
308                let mut z_row = vec![F::zero(); n_inst_cols];
309                // Fill in instruments: levels y_{eid, 0..t-n_lags-1}
310                // We pack them into a triangular block indexed by (t, lag_depth)
311                let avail = t - n_lags; // how many lagged levels available
312                let block_start: usize = if avail > 0 {
313                    (avail - 1) * avail / 2
314                } else {
315                    0
316                };
317                for s in 0..avail {
318                    let inst_t = t - n_lags - 1 - s; // level at time inst_t
319                                                     // safety check
320                    if inst_t < t_total {
321                        let flat_idx = block_start + s;
322                        if flat_idx < max_inst_per * (max_inst_per + 1) / 2 {
323                            z_row[flat_idx] = y[idx_map[eid][inst_t]];
324                        }
325                    }
326                }
327                // Also add levels of x as instruments (in the last kx columns)
328                let base = max_inst_per * (max_inst_per + 1) / 2;
329                for j in 0..kx {
330                    z_row[base + j] = x[[idx_map[eid][t], j]];
331                }
332                z_rows.push(z_row);
333            }
334        }
335        let z_flat: Vec<F> = z_rows.iter().flat_map(|r| r.iter().copied()).collect();
336        let z = Array2::from_shape_vec((nd, n_inst_cols), z_flat)
337            .map_err(|e| StatsError::ComputationError(format!("reshape z: {e}")))?;
338
339        // ── Step 1: initial W = (Z'H Z)^{-1} where H is block-diagonal ─────────
340        // Arellano-Bond use H = block-diag(H_i) where H_i is the first-difference
341        // covariance structure.  Simpler initialisation: W = (Z'Z)^{-1}.
342        let ztzt = matmul(&transpose(&z), &z)?;
343        let w1 = identity_if_singular(&ztzt)?;
344
345        // ── Step 1 GMM ───────────────────────────────────────────────────────────
346        let (coeffs1, resid1) = gmm_estimator(&dx, &dy, &z, &w1)?;
347
348        // ── Step 2: efficient W = (Z'diag(e²)Z)^{-1} ──────────────────────────
349        let mut meat = Array2::<F>::zeros((n_inst_cols, n_inst_cols));
350        for i in 0..nd {
351            let ei2 = resid1[i] * resid1[i];
352            for a in 0..n_inst_cols {
353                for b in 0..n_inst_cols {
354                    meat[[a, b]] = meat[[a, b]] + z[[i, a]] * z[[i, b]] * ei2;
355                }
356            }
357        }
358        let w2 = identity_if_singular(&meat)?;
359        let (coeffs2, resid2) = gmm_estimator(&dx, &dy, &z, &w2)?;
360
361        // ── Asymptotic covariance of β̂ ───────────────────────────────────────
362        // Var(β̂) = (X'ZWZ'X)^{-1} X'Z W Ṡ W Z'X (X'ZWZ'X)^{-1}
363        // where Ṡ = Z'diag(e²)Z.  We use a simpler sandwich.
364        let n_f = F::from_usize(nd).unwrap_or(F::one());
365        let ztx = matmul(&transpose(&z), &dx)?;
366        let wztx = matmul(&w2, &ztx)?;
367        let xtzwztx = matmul(&transpose(&ztx), &wztx)?;
368        let std_errors = gmm_se(&xtzwztx, &z, &dx, &resid2, &w2)?;
369
370        let z_stats: Array1<F> = coeffs2
371            .iter()
372            .zip(std_errors.iter())
373            .map(|(&c, &se)| if se > F::zero() { c / se } else { F::zero() })
374            .collect();
375
376        // ── Sargan test ───────────────────────────────────────────────────────
377        // J = e'Z(Z'Z)^{-1}Z'e / σ̂² ~ χ²(L - K)
378        let sargan = sargan_test(&resid2, &z, k_reg)?;
379
380        // ── Arellano-Bond AR tests ─────────────────────────────────────────────
381        let ar1 = ar_test(&resid2, nd, n_entities, 1);
382        let ar2 = ar_test(&resid2, nd, n_entities, 2);
383
384        Ok(DynamicPanelResult {
385            coefficients: coeffs2,
386            std_errors,
387            z_stats,
388            sargan,
389            ar1,
390            ar2,
391            n_obs: nd,
392            n_instruments: n_inst_cols,
393            residuals: resid2,
394        })
395    }
396}
397
398// ──────────────────────────────────────────────────────────────────────────────
399// BlundellBond (System GMM)
400// ──────────────────────────────────────────────────────────────────────────────
401
402/// Blundell-Bond (1998) system GMM estimator.
403///
404/// Combines Arellano-Bond first-difference equations with level equations,
405/// using lagged first-differences as instruments for levels.
406pub struct BlundellBond;
407
408impl BlundellBond {
409    /// Fit the Blundell-Bond system GMM estimator.
410    ///
411    /// # Arguments
412    /// * `y`       – response (N)
413    /// * `x`       – exogenous regressors (N × K)
414    /// * `entity`  – entity IDs (0-indexed)
415    /// * `time`    – time IDs (0-indexed)
416    /// * `n_lags`  – number of lagged y terms (typically 1)
417    pub fn fit<F>(
418        y: &ArrayView1<F>,
419        x: &ArrayView2<F>,
420        entity: &[usize],
421        time: &[usize],
422        n_lags: usize,
423    ) -> StatsResult<DynamicPanelResult<F>>
424    where
425        F: Float
426            + std::iter::Sum
427            + std::fmt::Debug
428            + std::fmt::Display
429            + scirs2_core::numeric::NumAssign
430            + scirs2_core::numeric::One
431            + scirs2_core::ndarray::ScalarOperand
432            + FromPrimitive
433            + Send
434            + Sync
435            + 'static,
436    {
437        let n = y.len();
438        let (nx, kx) = x.dim();
439        if nx != n || entity.len() != n || time.len() != n {
440            return Err(StatsError::DimensionMismatch(
441                "y, x, entity, time lengths must match".to_string(),
442            ));
443        }
444        let n_entities = entity.iter().copied().max().map(|m| m + 1).unwrap_or(0);
445        let t_total = time.iter().copied().max().map(|m| m + 1).unwrap_or(0);
446        if t_total < n_lags + 2 {
447            return Err(StatsError::InsufficientData(format!(
448                "Need >= {} time periods",
449                n_lags + 2
450            )));
451        }
452        let t_per = n / n_entities;
453        if t_per * n_entities != n {
454            return Err(StatsError::InvalidArgument(
455                "BB-GMM: requires balanced panel".to_string(),
456            ));
457        }
458
459        let mut idx_map = vec![vec![0usize; t_total]; n_entities];
460        for i in 0..n {
461            idx_map[entity[i]][time[i]] = i;
462        }
463
464        let t_fd_start = n_lags + 1; // first FD observation
465        let nd_fd = n_entities * (t_total - t_fd_start);
466        let nd_lev = n_entities * (t_total - n_lags - 1); // level eqs from t=n_lags+1
467
468        let n_total = nd_fd + nd_lev;
469        let k_reg = n_lags + kx;
470
471        // ── Build stacked (FD + level) data ────────────────────────────────────
472        let mut dy_vec: Vec<F> = Vec::with_capacity(n_total);
473        let mut dx_rows: Vec<Vec<F>> = Vec::with_capacity(n_total);
474        let mut z_rows: Vec<Vec<F>> = Vec::with_capacity(n_total);
475
476        // AB instruments: triangular block (same as ArellanoBlond)
477        let max_inst_ab = t_total - n_lags - 1;
478        let n_inst_ab = max_inst_ab * (max_inst_ab + 1) / 2;
479        let n_inst_bb = t_total - n_lags - 1; // Δy_{t-1} for level eq
480        let n_inst_cols = n_inst_ab + kx + n_inst_bb + kx;
481
482        // FD equations
483        for eid in 0..n_entities {
484            for t in t_fd_start..t_total {
485                let i_cur = idx_map[eid][t];
486                let i_prev = idx_map[eid][t - 1];
487                let dy = y[i_cur] - y[i_prev];
488                dy_vec.push(dy);
489
490                let mut row: Vec<F> = Vec::new();
491                for lag in 1..=n_lags {
492                    if t >= lag + 1 {
493                        let il = idx_map[eid][t - lag];
494                        let il1 = idx_map[eid][t - lag - 1];
495                        row.push(y[il] - y[il1]);
496                    } else {
497                        row.push(F::zero());
498                    }
499                }
500                for j in 0..kx {
501                    row.push(x[[i_cur, j]] - x[[i_prev, j]]);
502                }
503                dx_rows.push(row);
504
505                // Instrument row
506                let mut z_row = vec![F::zero(); n_inst_cols];
507                let avail = t - n_lags;
508                let block_start = if avail > 0 {
509                    (avail - 1) * avail / 2
510                } else {
511                    0
512                };
513                for s in 0..avail {
514                    let inst_t = t - n_lags - 1 - s;
515                    if inst_t < t_total {
516                        let fi = block_start + s;
517                        if fi < n_inst_ab {
518                            z_row[fi] = y[idx_map[eid][inst_t]];
519                        }
520                    }
521                }
522                for j in 0..kx {
523                    z_row[n_inst_ab + j] = x[[i_cur, j]];
524                }
525                z_rows.push(z_row);
526            }
527        }
528
529        // Level equations (BB adds): y_{it} = α y_{i,t-1} + x_{it}β + u_i + ε_{it}
530        // instruments: Δy_{i,t-1}
531        for eid in 0..n_entities {
532            for t in (n_lags + 1)..t_total {
533                let i_cur = idx_map[eid][t];
534                let i_prev = idx_map[eid][t - 1];
535                dy_vec.push(y[i_cur]); // levels
536                let mut row: Vec<F> = Vec::new();
537                for lag in 1..=n_lags {
538                    row.push(y[idx_map[eid][t - lag]]);
539                }
540                for j in 0..kx {
541                    row.push(x[[i_cur, j]]);
542                }
543                dx_rows.push(row);
544
545                let mut z_row = vec![F::zero(); n_inst_cols];
546                // instrument: Δy_{i,t-1}
547                if t >= n_lags + 1 {
548                    let lag_idx = n_inst_ab + kx + (t - n_lags - 1).min(n_inst_bb - 1);
549                    let dy_lag = y[i_prev]
550                        - if t >= 2 {
551                            y[idx_map[eid][t - 2]]
552                        } else {
553                            F::zero()
554                        };
555                    if lag_idx < n_inst_cols - kx {
556                        z_row[lag_idx] = dy_lag;
557                    }
558                }
559                for j in 0..kx {
560                    z_row[n_inst_ab + kx + n_inst_bb + j] = x[[i_cur, j]];
561                }
562                z_rows.push(z_row);
563            }
564        }
565
566        let n_all = dy_vec.len();
567        let dx_flat: Vec<F> = dx_rows.iter().flat_map(|r| r.iter().copied()).collect();
568        let z_flat: Vec<F> = z_rows.iter().flat_map(|r| r.iter().copied()).collect();
569        let dx = Array2::from_shape_vec((n_all, k_reg), dx_flat)
570            .map_err(|e| StatsError::ComputationError(format!("reshape dx: {e}")))?;
571        let dy = Array1::from(dy_vec);
572        let z = Array2::from_shape_vec((n_all, n_inst_cols), z_flat)
573            .map_err(|e| StatsError::ComputationError(format!("reshape z: {e}")))?;
574
575        // Step 1
576        let ztzt = matmul(&transpose(&z), &z)?;
577        let w1 = identity_if_singular(&ztzt)?;
578        let (coeffs1, resid1) = gmm_estimator(&dx, &dy, &z, &w1)?;
579
580        // Step 2
581        let mut meat = Array2::<F>::zeros((n_inst_cols, n_inst_cols));
582        for i in 0..n_all {
583            let ei2 = resid1[i] * resid1[i];
584            for a in 0..n_inst_cols {
585                for b in 0..n_inst_cols {
586                    meat[[a, b]] = meat[[a, b]] + z[[i, a]] * z[[i, b]] * ei2;
587                }
588            }
589        }
590        let w2 = identity_if_singular(&meat)?;
591        let (coeffs2, resid2) = gmm_estimator(&dx, &dy, &z, &w2)?;
592
593        let ztx = matmul(&transpose(&z), &dx)?;
594        let wztx = matmul(&w2, &ztx)?;
595        let xtzwztx = matmul(&transpose(&ztx), &wztx)?;
596        let std_errors = gmm_se(&xtzwztx, &z, &dx, &resid2, &w2)?;
597        let z_stats: Array1<F> = coeffs2
598            .iter()
599            .zip(std_errors.iter())
600            .map(|(&c, &se)| if se > F::zero() { c / se } else { F::zero() })
601            .collect();
602
603        let sargan = sargan_test(&resid2, &z, k_reg)?;
604        let ar1 = ar_test(&resid2, n_all, n_entities, 1);
605        let ar2 = ar_test(&resid2, n_all, n_entities, 2);
606
607        Ok(DynamicPanelResult {
608            coefficients: coeffs2,
609            std_errors,
610            z_stats,
611            sargan,
612            ar1,
613            ar2,
614            n_obs: n_all,
615            n_instruments: n_inst_cols,
616            residuals: resid2,
617        })
618    }
619}
620
621// ──────────────────────────────────────────────────────────────────────────────
622// SarganTest (standalone)
623// ──────────────────────────────────────────────────────────────────────────────
624
625/// Sargan test for over-identifying restrictions.
626///
627/// J = e' Z (Z'Z)^{-1} Z' e / σ̂²  ~  χ²(L - K)
628pub struct SarganTest;
629
630impl SarganTest {
631    /// Compute Sargan J-statistic from residuals and instrument matrix.
632    pub fn test<F>(
633        residuals: &ArrayView1<F>,
634        z: &ArrayView2<F>,
635        n_regressors: usize,
636    ) -> StatsResult<SarganTestResult<F>>
637    where
638        F: Float
639            + std::iter::Sum
640            + std::fmt::Debug
641            + std::fmt::Display
642            + scirs2_core::numeric::NumAssign
643            + scirs2_core::numeric::One
644            + scirs2_core::ndarray::ScalarOperand
645            + FromPrimitive
646            + Send
647            + Sync
648            + 'static,
649    {
650        let e_owned = residuals.to_owned();
651        let z_owned = z.to_owned();
652        sargan_test(&e_owned, &z_owned, n_regressors)
653    }
654}
655
656// ──────────────────────────────────────────────────────────────────────────────
657// Private helpers
658// ──────────────────────────────────────────────────────────────────────────────
659
660/// Compute Sargan J-statistic.
661fn sargan_test<F>(e: &Array1<F>, z: &Array2<F>, k: usize) -> StatsResult<SarganTestResult<F>>
662where
663    F: Float
664        + std::iter::Sum
665        + std::fmt::Debug
666        + std::fmt::Display
667        + scirs2_core::numeric::NumAssign
668        + scirs2_core::numeric::One
669        + scirs2_core::ndarray::ScalarOperand
670        + FromPrimitive
671        + Send
672        + Sync
673        + 'static,
674{
675    let (nd, l) = z.dim();
676    let ze = mat_vec(&transpose(z), e)?; // L-vector Z'e
677    let ztz = matmul(&transpose(z), z)?;
678    let inv_ztz_ze = solve(&ztz.view(), &ze.view(), None)
679        .map_err(|err| StatsError::ComputationError(format!("Sargan solve: {err}")))?;
680
681    let j_num: F = ze.iter().zip(inv_ztz_ze.iter()).map(|(&a, &b)| a * b).sum();
682    let n_f = F::from_usize(nd).unwrap_or(F::one());
683    let sigma2 = e.iter().map(|&r| r * r).sum::<F>() / n_f;
684    let j_stat = if sigma2 > F::zero() {
685        j_num / sigma2
686    } else {
687        F::zero()
688    };
689
690    let df = if l > k { l - k } else { 1 };
691    let p_value = chi2_upper_pvalue(j_stat, df);
692    Ok(SarganTestResult {
693        j_stat,
694        df,
695        p_value,
696    })
697}
698
699/// Arellano-Bond serial correlation test of order `order`.
700fn ar_test<F: Float + FromPrimitive>(
701    e: &Array1<F>,
702    nd: usize,
703    n_entities: usize,
704    order: usize,
705) -> ARTestResult<F> {
706    if nd <= order || n_entities == 0 {
707        return ARTestResult {
708            z_stat: F::zero(),
709            p_value: F::one(),
710        };
711    }
712    let t_per = nd / n_entities;
713    if t_per < order + 1 {
714        return ARTestResult {
715            z_stat: F::zero(),
716            p_value: F::one(),
717        };
718    }
719    // AR(m) z-stat: correlation between e_{it} and e_{i,t-m}
720    let mut num = F::zero();
721    let mut denom_ee = F::zero();
722    let mut denom_elag = F::zero();
723    let mut count = 0usize;
724    for eid in 0..n_entities {
725        let base = eid * t_per;
726        for t in order..t_per {
727            let e_t = e[base + t];
728            let e_lag = e[base + t - order];
729            num = num + e_t * e_lag;
730            denom_ee = denom_ee + e_t * e_t;
731            denom_elag = denom_elag + e_lag * e_lag;
732            count += 1;
733        }
734    }
735    if count == 0 || denom_ee <= F::zero() || denom_elag <= F::zero() {
736        return ARTestResult {
737            z_stat: F::zero(),
738            p_value: F::one(),
739        };
740    }
741    let n_f = F::from_usize(count).unwrap_or(F::one());
742    let var_approx = (denom_ee * denom_elag).sqrt() / n_f;
743    let z_stat = if var_approx > F::zero() {
744        num / var_approx
745    } else {
746        F::zero()
747    };
748    let p_value = two_sided_normal_pvalue(z_stat);
749    ARTestResult { z_stat, p_value }
750}
751
752/// GMM robust SE = sqrt(diag(V)) where V = (X'ZWZ'X)^{-1}.
753fn gmm_se<F>(
754    xtzwztx: &Array2<F>,
755    z: &Array2<F>,
756    x: &Array2<F>,
757    e: &Array1<F>,
758    w: &Array2<F>,
759) -> StatsResult<Array1<F>>
760where
761    F: Float
762        + std::iter::Sum
763        + std::fmt::Debug
764        + std::fmt::Display
765        + scirs2_core::numeric::NumAssign
766        + scirs2_core::numeric::One
767        + scirs2_core::ndarray::ScalarOperand
768        + FromPrimitive
769        + Send
770        + Sync
771        + 'static,
772{
773    let k = xtzwztx.nrows();
774    // Sandwich: V = (X'ZWZ'X)^{-1} (X'ZW Ṡ WZ'X) (X'ZWZ'X)^{-1}
775    // Ṡ = Z'diag(e²)Z
776    let (nd, l) = z.dim();
777    let mut s_meat = Array2::<F>::zeros((l, l));
778    for i in 0..nd {
779        let ei2 = e[i] * e[i];
780        for a in 0..l {
781            for b in 0..l {
782                s_meat[[a, b]] = s_meat[[a, b]] + z[[i, a]] * z[[i, b]] * ei2;
783            }
784        }
785    }
786    // middle = (X'ZWZ'X)^{-1} X'Z W Ṡ W Z'X (X'ZWZ'X)^{-1}
787    // ≈ diagonal approx: σ̂² * diag((X'ZWZ'X)^{-1})
788    let n_f = F::from_usize(nd).unwrap_or(F::one());
789    let k_f = F::from_usize(k).unwrap_or(F::one());
790    let sigma2 = e.iter().map(|&r| r * r).sum::<F>() / (n_f - k_f);
791
792    let mut se = Array1::zeros(k);
793    for j in 0..k {
794        let mut ej = Array1::zeros(k);
795        ej[j] = F::one();
796        let vj = solve(&xtzwztx.view(), &ej.view(), None)
797            .map_err(|e2| StatsError::ComputationError(format!("gmm_se solve: {e2}")))?;
798        let var_j = vj[j] * sigma2;
799        se[j] = if var_j >= F::zero() {
800            var_j.sqrt()
801        } else {
802            F::zero()
803        };
804    }
805    Ok(se)
806}
807
808/// Return I_L if matrix is singular (near-zero det), else try to invert via solve.
809fn identity_if_singular<F>(m: &Array2<F>) -> StatsResult<Array2<F>>
810where
811    F: Float
812        + std::iter::Sum
813        + std::fmt::Debug
814        + std::fmt::Display
815        + scirs2_core::numeric::NumAssign
816        + scirs2_core::numeric::One
817        + scirs2_core::ndarray::ScalarOperand
818        + FromPrimitive
819        + Send
820        + Sync
821        + 'static,
822{
823    let k = m.nrows();
824    // Try to invert column by column
825    let mut inv = Array2::zeros((k, k));
826    let mut ok = true;
827    for j in 0..k {
828        let mut ej = Array1::zeros(k);
829        ej[j] = F::one();
830        match solve(&m.view(), &ej.view(), None) {
831            Ok(v) => {
832                for i in 0..k {
833                    inv[[i, j]] = v[i];
834                }
835            }
836            Err(_) => {
837                ok = false;
838                break;
839            }
840        }
841    }
842    if ok {
843        Ok(inv)
844    } else {
845        // Fall back to identity
846        let mut id = Array2::zeros((k, k));
847        for i in 0..k {
848            id[[i, i]] = F::one();
849        }
850        Ok(id)
851    }
852}
853
854/// Chi-squared upper-tail p-value (Wilson-Hilferty).
855fn chi2_upper_pvalue<F: Float + FromPrimitive>(chi2: F, df: usize) -> F {
856    if chi2 <= F::zero() {
857        return F::one();
858    }
859    let k = F::from_usize(df).unwrap_or(F::one());
860    let two = F::from_f64(2.0).unwrap_or(F::one());
861    let nine = F::from_f64(9.0).unwrap_or(F::one());
862    let factor = two / (nine * k);
863    let x = (chi2 / k).cbrt();
864    let mu = F::one() - factor;
865    let sigma = factor.sqrt();
866    let z = (x - mu) / sigma;
867    p_normal_upper(z)
868}
869
870/// Two-sided N(0,1) p-value.
871fn two_sided_normal_pvalue<F: Float + FromPrimitive>(z: F) -> F {
872    let two = F::from_f64(2.0).unwrap_or(F::one());
873    let abs_z = if z < F::zero() { -z } else { z };
874    let p = p_normal_upper(abs_z);
875    let two_p = two * p;
876    if two_p > F::one() {
877        F::one()
878    } else {
879        two_p
880    }
881}
882
883fn p_normal_upper<F: Float + FromPrimitive>(z: F) -> F {
884    let p1 = F::from_f64(0.2316419).unwrap_or(F::zero());
885    let b1 = F::from_f64(0.319381530).unwrap_or(F::zero());
886    let b2 = F::from_f64(-0.356563782).unwrap_or(F::zero());
887    let b3 = F::from_f64(1.781477937).unwrap_or(F::zero());
888    let b4 = F::from_f64(-1.821255978).unwrap_or(F::zero());
889    let b5 = F::from_f64(1.330274429).unwrap_or(F::zero());
890    let sqrt2pi_inv = F::from_f64(0.39894228).unwrap_or(F::zero());
891    let two = F::from_f64(2.0).unwrap_or(F::one());
892
893    let abs_z = if z < F::zero() { -z } else { z };
894    let t = F::one() / (F::one() + p1 * abs_z);
895    let poly = t * (b1 + t * (b2 + t * (b3 + t * (b4 + t * b5))));
896    let phi = sqrt2pi_inv * (-(abs_z * abs_z) / two).exp();
897    let p_upper = (phi * poly).max(F::zero()).min(F::one());
898    if z >= F::zero() {
899        p_upper
900    } else {
901        F::one() - p_upper
902    }
903}
904
905// ──────────────────────────────────────────────────────────────────────────────
906// Tests
907// ──────────────────────────────────────────────────────────────────────────────
908
909#[cfg(test)]
910mod tests {
911    use super::*;
912    use scirs2_core::ndarray::{Array1, Array2};
913
914    fn make_dynamic_panel(
915        n_ent: usize,
916        t_per: usize,
917        alpha: f64,
918        beta: f64,
919    ) -> (Array1<f64>, Array2<f64>, Vec<usize>, Vec<usize>) {
920        let n = n_ent * t_per;
921        let entity: Vec<usize> = (0..n_ent)
922            .flat_map(|e| std::iter::repeat(e).take(t_per))
923            .collect();
924        let time: Vec<usize> = (0..t_per).cycle().take(n).collect();
925        let mut x_vals = vec![0.0_f64; n];
926        let mut y_vals = vec![0.0_f64; n];
927        for eid in 0..n_ent {
928            let u_i = (eid as f64) * 0.2;
929            for t in 0..t_per {
930                let idx = eid * t_per + t;
931                x_vals[idx] = (t as f64) * 0.5 + 1.0;
932                if t == 0 {
933                    y_vals[idx] = u_i + x_vals[idx] * beta + 0.1;
934                } else {
935                    let idx_prev = eid * t_per + t - 1;
936                    y_vals[idx] =
937                        alpha * y_vals[idx_prev] + beta * x_vals[idx] + u_i + 0.01 * (t as f64);
938                }
939            }
940        }
941        let x = Array2::from_shape_vec((n, 1), x_vals).unwrap();
942        let y = Array1::from(y_vals);
943        (y, x, entity, time)
944    }
945
946    #[test]
947    fn test_arellano_bond_fit() {
948        let (y, x, entity, time) = make_dynamic_panel(8, 6, 0.5, 1.0);
949        let result =
950            ArellanoBlond::fit(&y.view(), &x.view(), &entity, &time, 1).expect("AB-GMM failed");
951        // alpha ≈ 0.5, beta ≈ 1.0 approximately
952        assert!(result.n_obs > 0);
953        assert_eq!(result.coefficients.len(), 2); // 1 lag + 1 regressor
954        assert!(result.sargan.p_value >= 0.0);
955    }
956
957    #[test]
958    fn test_blundell_bond_fit() {
959        let (y, x, entity, time) = make_dynamic_panel(8, 6, 0.5, 1.0);
960        let result =
961            BlundellBond::fit(&y.view(), &x.view(), &entity, &time, 1).expect("BB-GMM failed");
962        assert!(result.n_obs > 0);
963        assert_eq!(result.coefficients.len(), 2);
964    }
965
966    #[test]
967    fn test_sargan_test_standalone() {
968        let (y, x, entity, time) = make_dynamic_panel(8, 6, 0.5, 1.0);
969        let result =
970            ArellanoBlond::fit(&y.view(), &x.view(), &entity, &time, 1).expect("AB-GMM failed");
971        // J-stat should be non-negative
972        assert!(result.sargan.j_stat >= 0.0);
973        assert!(result.sargan.p_value >= 0.0 && result.sargan.p_value <= 1.0);
974    }
975}