use crate::error::{StatsError, StatsResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
#[derive(Debug, Clone)]
pub struct IVResult {
pub coefficients: Array1<f64>,
pub std_errors: Array1<f64>,
pub t_stats: Array1<f64>,
pub p_values: Array1<f64>,
pub conf_intervals: Array2<f64>,
pub n_obs: usize,
pub n_endog: usize,
pub n_instruments: usize,
pub first_stage_f: Option<f64>,
pub partial_r_squared: Option<f64>,
pub j_statistic: Option<f64>,
pub j_p_value: Option<f64>,
pub rss: f64,
pub estimator: String,
}
#[derive(Debug, Clone)]
pub struct HausmanResult {
pub statistic: f64,
pub p_value: f64,
pub df: usize,
pub coef_difference: Array1<f64>,
pub endogenous_detected: bool,
}
#[derive(Debug, Clone)]
pub struct WeakInstrumentResult {
pub f_statistics: Vec<f64>,
pub partial_r_squared: Vec<f64>,
pub critical_value_10pct: f64,
pub instruments_weak: Vec<bool>,
}
fn ols_fit(
x: &ArrayView2<f64>,
y: &ArrayView1<f64>,
) -> StatsResult<(Array1<f64>, Array1<f64>, Array2<f64>)> {
let n = x.nrows();
let k = x.ncols();
if n < k {
return Err(StatsError::InsufficientData(format!(
"OLS requires at least {k} observations, got {n}"
)));
}
let xtx = x.t().dot(x);
let xty = x.t().dot(y);
let xtx_inv = cholesky_invert(&xtx.view())?;
let beta = xtx_inv.dot(&xty);
let fitted = x.dot(&beta);
let residuals = y.to_owned() - fitted;
Ok((beta, residuals, xtx_inv))
}
fn cholesky_invert(a: &ArrayView2<f64>) -> StatsResult<Array2<f64>> {
let n = a.nrows();
if a.ncols() != n {
return Err(StatsError::DimensionMismatch(
"Matrix must be square for inversion".into(),
));
}
let mut l = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..=i {
let mut s = a[[i, j]];
for p in 0..j {
s -= l[[i, p]] * l[[j, p]];
}
if i == j {
if s <= 0.0 {
return Err(StatsError::ComputationError(
"Matrix is not positive definite; check for multicollinearity".into(),
));
}
l[[i, j]] = s.sqrt();
} else {
l[[i, j]] = s / l[[j, j]];
}
}
}
let mut linv = Array2::<f64>::zeros((n, n));
for j in 0..n {
linv[[j, j]] = 1.0 / l[[j, j]];
for i in (j + 1)..n {
let mut s = 0.0_f64;
for p in j..i {
s += l[[i, p]] * linv[[p, j]];
}
linv[[i, j]] = -s / l[[i, i]];
}
}
let inv = linv.t().dot(&linv);
Ok(inv)
}
fn t_dist_p_value(t: f64, df: f64) -> f64 {
if df <= 0.0 {
return 1.0;
}
let x = df / (df + t * t);
let a = df / 2.0;
let b = 0.5_f64;
let ibeta = regularized_incomplete_beta(x, a, b);
ibeta.min(1.0).max(0.0)
}
fn chi2_p_value(x: f64, df: usize) -> f64 {
if x <= 0.0 {
return 1.0;
}
let df_f = df as f64;
1.0 - regularized_gamma_lower(df_f / 2.0, x / 2.0)
}
fn regularized_gamma_lower(a: f64, x: f64) -> f64 {
if x < 0.0 {
return 0.0;
}
if x == 0.0 {
return 0.0;
}
if x < a + 1.0 {
let mut ap = a;
let mut del = 1.0 / a;
let mut sum = del;
for _ in 0..200 {
ap += 1.0;
del *= x / ap;
sum += del;
if del.abs() < sum.abs() * 3e-15 {
break;
}
}
sum * (-x + a * x.ln() - ln_gamma(a)).exp()
} else {
1.0 - regularized_gamma_upper_cf(a, x)
}
}
fn regularized_gamma_upper_cf(a: f64, x: f64) -> f64 {
let fpmin = 1e-300_f64;
let mut b = x + 1.0 - a;
let mut c = 1.0 / fpmin;
let mut d = 1.0 / b;
let mut h = d;
for i in 1..=200_i64 {
let an = -(i as f64) * ((i as f64) - a);
b += 2.0;
d = an * d + b;
if d.abs() < fpmin {
d = fpmin;
}
c = b + an / c;
if c.abs() < fpmin {
c = fpmin;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < 3e-15 {
break;
}
}
(-x + a * x.ln() - ln_gamma(a)).exp() * h
}
fn ln_gamma(x: f64) -> f64 {
const G: f64 = 7.0;
const C: [f64; 9] = [
0.99999999999980993,
676.5203681218851,
-1259.1392167224028,
771.323_428_777_653_1,
-176.615_029_162_140_6,
12.507_343_278_686_905,
-0.13857_109_526_572_012,
9.984_369_578_019_572e-6,
1.5056_327_351_493_116e-7,
];
if x < 0.5 {
std::f64::consts::PI.ln() - (std::f64::consts::PI * x).sin().ln() - ln_gamma(1.0 - x)
} else {
let z = x - 1.0;
let mut s = C[0];
for (i, &ci) in C[1..].iter().enumerate() {
s += ci / (z + (i as f64) + 1.0);
}
let t = z + G + 0.5;
0.5 * (2.0 * std::f64::consts::PI).ln() + (z + 0.5) * t.ln() - t + s.ln()
}
}
fn regularized_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
if x >= 1.0 {
return 1.0;
}
if x > (a + 1.0) / (a + b + 2.0) {
return 1.0 - regularized_incomplete_beta(1.0 - x, b, a);
}
let log_beta_cf =
(a * x.ln() + b * (1.0 - x).ln() - ln_gamma(a) - ln_gamma(b) + ln_gamma(a + b)).exp() / a;
log_beta_cf * beta_cf(x, a, b)
}
fn beta_cf(x: f64, a: f64, b: f64) -> f64 {
let fpmin = 1e-300_f64;
let qab = a + b;
let qap = a + 1.0;
let qam = a - 1.0;
let mut c = 1.0_f64;
let mut d = 1.0 - qab * x / qap;
if d.abs() < fpmin {
d = fpmin;
}
d = 1.0 / d;
let mut h = d;
for m in 1..=200_i32 {
let m_f = m as f64;
let aa = m_f * (b - m_f) * x / ((qam + 2.0 * m_f) * (a + 2.0 * m_f));
d = 1.0 + aa * d;
if d.abs() < fpmin {
d = fpmin;
}
c = 1.0 + aa / c;
if c.abs() < fpmin {
c = fpmin;
}
d = 1.0 / d;
h *= d * c;
let aa = -(a + m_f) * (qab + m_f) * x / ((a + 2.0 * m_f) * (qap + 2.0 * m_f));
d = 1.0 + aa * d;
if d.abs() < fpmin {
d = fpmin;
}
c = 1.0 + aa / c;
if c.abs() < fpmin {
c = fpmin;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < 3e-15 {
break;
}
}
h
}
pub struct IVEstimator {
pub robust_se: bool,
}
impl IVEstimator {
pub fn new(robust_se: bool) -> Self {
Self { robust_se }
}
pub fn fit(
&self,
y: &ArrayView1<f64>,
x_endog: &ArrayView2<f64>,
x_exog: &ArrayView2<f64>,
z_excl: &ArrayView2<f64>,
) -> StatsResult<IVResult> {
let n = y.len();
let k_endog = x_endog.ncols();
let k_exog = x_exog.ncols();
let l_excl = z_excl.ncols();
if n < 2 {
return Err(StatsError::InsufficientData(
"Need at least 2 observations".into(),
));
}
if x_endog.nrows() != n || x_exog.nrows() != n || z_excl.nrows() != n {
return Err(StatsError::DimensionMismatch(
"All matrices must have the same number of rows as y".into(),
));
}
if l_excl < k_endog {
return Err(StatsError::InvalidArgument(format!(
"Need at least {k_endog} instruments for {k_endog} endogenous regressors, got {l_excl}"
)));
}
let k_total = k_endog + k_exog;
let l_total = l_excl + k_exog;
let mut z = Array2::<f64>::zeros((n, l_total));
for i in 0..n {
for j in 0..l_excl {
z[[i, j]] = z_excl[[i, j]];
}
for j in 0..k_exog {
z[[i, l_excl + j]] = x_exog[[i, j]];
}
}
let mut x = Array2::<f64>::zeros((n, k_total));
for i in 0..n {
for j in 0..k_endog {
x[[i, j]] = x_endog[[i, j]];
}
for j in 0..k_exog {
x[[i, k_endog + j]] = x_exog[[i, j]];
}
}
let (_, _, zt_z_inv) = ols_fit(&z.view(), &y)?; let mut x_hat = Array2::<f64>::zeros((n, k_total));
let mut first_stage_f = None;
let mut partial_r2 = None;
if k_endog > 0 {
let (_, _, ztz_inv) = ols_fit(&z.view(), &Array1::zeros(n).view())?;
let ztz_inv_real = cholesky_invert(&z.t().dot(&z).view())?;
let mut fs_f_sum = 0.0_f64;
let mut pr2_sum = 0.0_f64;
for j in 0..k_endog {
let xj = x.column(j).to_owned();
let coef = ztz_inv_real.dot(&z.t().dot(&xj));
let x_hat_j = z.dot(&coef);
for i in 0..n {
x_hat[[i, j]] = x_hat_j[i];
}
let (fs_f, pr2) = first_stage_diagnostics(&z.view(), &xj.view(), l_excl, k_exog)?;
fs_f_sum += fs_f;
pr2_sum += pr2;
}
for j in 0..k_exog {
for i in 0..n {
x_hat[[i, k_endog + j]] = x_exog[[i, j]];
}
}
first_stage_f = Some(fs_f_sum / k_endog as f64);
partial_r2 = Some(pr2_sum / k_endog as f64);
let _ = ztz_inv; } else {
x_hat.assign(&x);
}
let xht_x = x_hat.t().dot(&x);
let xht_y = x_hat.t().dot(y);
let xhtx_inv = cholesky_invert(&xht_x.view())?;
let beta = xhtx_inv.dot(&xht_y);
let y_hat = x.dot(&beta);
let residuals = y.to_owned() - &y_hat;
let rss: f64 = residuals.iter().map(|&r| r * r).sum();
let df = (n - k_total) as f64;
if df <= 0.0 {
return Err(StatsError::InsufficientData(
"Insufficient degrees of freedom".into(),
));
}
let s2 = rss / df;
let vcov = if self.robust_se {
let scale = n as f64 / (n as f64 - k_total as f64);
let mut meat = Array2::<f64>::zeros((k_total, k_total));
for i in 0..n {
let xi = x.row(i);
let ui2 = residuals[i] * residuals[i];
for r in 0..k_total {
for c in 0..k_total {
meat[[r, c]] += ui2 * xi[r] * xi[c];
}
}
}
let bread_left = xhtx_inv.dot(&x_hat.t());
let inner = bread_left.dot(&meat);
inner.dot(&xhtx_inv.t()) * scale
} else {
xhtx_inv.mapv(|v| v * s2)
};
let std_errors: Array1<f64> = (0..k_total).map(|i| vcov[[i, i]].max(0.0).sqrt()).collect();
let t_stats: Array1<f64> = (0..k_total)
.map(|i| {
if std_errors[i] > 0.0 {
beta[i] / std_errors[i]
} else {
0.0
}
})
.collect();
let p_values: Array1<f64> = t_stats.iter().map(|&t| t_dist_p_value(t, df)).collect();
let mut conf_intervals = Array2::<f64>::zeros((k_total, 2));
let t_crit = t_critical(0.025, df as usize);
for i in 0..k_total {
conf_intervals[[i, 0]] = beta[i] - t_crit * std_errors[i];
conf_intervals[[i, 1]] = beta[i] + t_crit * std_errors[i];
}
let (j_statistic, j_p_value) = if l_excl > k_endog {
let (_, residuals_z, _) = ols_fit(&z.view(), &residuals.view())?;
let ss_res: f64 = residuals_z.iter().map(|&r| r * r).sum();
let ss_tot: f64 = residuals.iter().map(|&r| r * r).sum();
let r2_z = 1.0 - ss_res / ss_tot.max(1e-15);
let j_stat = n as f64 * r2_z;
let j_df = l_excl - k_endog;
let j_p = chi2_p_value(j_stat, j_df);
(Some(j_stat), Some(j_p))
} else {
(None, None)
};
Ok(IVResult {
coefficients: beta,
std_errors,
t_stats,
p_values,
conf_intervals,
n_obs: n,
n_endog: k_endog,
n_instruments: l_excl,
first_stage_f,
partial_r_squared: partial_r2,
j_statistic,
j_p_value,
rss,
estimator: "2SLS".into(),
})
}
}
fn first_stage_diagnostics(
z: &ArrayView2<f64>,
x_endog: &ArrayView1<f64>,
l_excl: usize,
k_exog: usize,
) -> StatsResult<(f64, f64)> {
let n = x_endog.len();
let l_total = z.ncols();
let (_, resid_u, _) = ols_fit(z, x_endog)?;
let rss_u: f64 = resid_u.iter().map(|&r| r * r).sum();
let x_exog_mat = z.slice(scirs2_core::ndarray::s![.., l_excl..l_total]);
let (_, resid_r, _) = ols_fit(&x_exog_mat, x_endog)?;
let rss_r: f64 = resid_r.iter().map(|&r| r * r).sum();
let df_num = l_excl as f64;
let df_den = (n as f64) - (l_total as f64);
if df_den <= 0.0 || df_num <= 0.0 {
return Ok((0.0, 0.0));
}
let f_stat = ((rss_r - rss_u) / df_num) / (rss_u / df_den).max(1e-15);
let partial_r2 = if rss_r > 1e-15 {
(rss_r - rss_u) / rss_r
} else {
0.0
};
Ok((f_stat.max(0.0), partial_r2.max(0.0).min(1.0)))
}
fn t_critical(alpha: f64, df: usize) -> f64 {
let df_f = df as f64;
let mut t = 2.0_f64; for _ in 0..50 {
let p = t_dist_p_value(t, df_f);
let two_tail_target = 2.0 * alpha;
let err = p - two_tail_target;
let delta = 1e-6;
let dp = (t_dist_p_value(t + delta, df_f) - p) / delta;
if dp.abs() < 1e-15 {
break;
}
t -= err / dp;
if err.abs() < 1e-10 {
break;
}
}
t.max(0.0)
}
pub struct LIML;
impl LIML {
pub fn fit(
y: &ArrayView1<f64>,
x_endog: &ArrayView2<f64>,
x_exog: &ArrayView2<f64>,
z_excl: &ArrayView2<f64>,
) -> StatsResult<IVResult> {
let n = y.len();
let k_endog = x_endog.ncols();
let k_exog = x_exog.ncols();
let l_excl = z_excl.ncols();
let k_total = k_endog + k_exog;
if n < 2 {
return Err(StatsError::InsufficientData(
"Need at least 2 observations".into(),
));
}
if l_excl < k_endog {
return Err(StatsError::InvalidArgument(format!(
"Need at least {k_endog} instruments, got {l_excl}"
)));
}
let mut w = Array2::<f64>::zeros((n, 1 + k_endog + k_exog));
for i in 0..n {
w[[i, 0]] = y[i];
for j in 0..k_endog {
w[[i, 1 + j]] = x_endog[[i, j]];
}
for j in 0..k_exog {
w[[i, 1 + k_endog + j]] = x_exog[[i, j]];
}
}
let mut z = Array2::<f64>::zeros((n, l_excl + k_exog));
for i in 0..n {
for j in 0..l_excl {
z[[i, j]] = z_excl[[i, j]];
}
for j in 0..k_exog {
z[[i, l_excl + j]] = x_exog[[i, j]];
}
}
let m0_w = annihilate(x_exog, &w.view())?;
let m_w = annihilate(&z.view(), &w.view())?;
let a = m0_w.t().dot(&m0_w); let b = m_w.t().dot(&m_w);
let b_inv = cholesky_invert(&b.view())?;
let c = b_inv.dot(&a);
let lambda_liml = min_eigenvalue(&c.view())?;
let scale = lambda_liml;
let mut x = Array2::<f64>::zeros((n, k_total));
for i in 0..n {
for j in 0..k_endog {
x[[i, j]] = x_endog[[i, j]];
}
for j in 0..k_exog {
x[[i, k_endog + j]] = x_exog[[i, j]];
}
}
let m0_x = annihilate(x_exog, &x.view())?;
let m_x = annihilate(&z.view(), &x.view())?;
let m0_y = annihilate_vec(x_exog, y)?;
let m_y = annihilate_vec(&z.view(), y)?;
let lhs_mat = m0_x.t().dot(&m0_x) - m_x.t().dot(&m_x) * scale;
let rhs_vec = m0_x.t().dot(&m0_y) - m_x.t().dot(&m_y) * scale;
let lhs_inv = cholesky_invert(&lhs_mat.view())?;
let beta = lhs_inv.dot(&rhs_vec);
let y_hat = x.dot(&beta);
let residuals = y.to_owned() - &y_hat;
let rss: f64 = residuals.iter().map(|&r| r * r).sum();
let df = (n - k_total) as f64;
let s2 = if df > 0.0 { rss / df } else { rss };
let vcov = lhs_inv.mapv(|v| v * s2);
let std_errors: Array1<f64> = (0..k_total).map(|i| vcov[[i, i]].max(0.0).sqrt()).collect();
let t_stats: Array1<f64> = (0..k_total)
.map(|i| {
if std_errors[i] > 0.0 {
beta[i] / std_errors[i]
} else {
0.0
}
})
.collect();
let p_values: Array1<f64> = t_stats
.iter()
.map(|&t| t_dist_p_value(t, df.max(1.0)))
.collect();
let t_crit = t_critical(0.025, df as usize);
let mut conf_intervals = Array2::<f64>::zeros((k_total, 2));
for i in 0..k_total {
conf_intervals[[i, 0]] = beta[i] - t_crit * std_errors[i];
conf_intervals[[i, 1]] = beta[i] + t_crit * std_errors[i];
}
Ok(IVResult {
coefficients: beta,
std_errors,
t_stats,
p_values,
conf_intervals,
n_obs: n,
n_endog: k_endog,
n_instruments: l_excl,
first_stage_f: None,
partial_r_squared: None,
j_statistic: None,
j_p_value: None,
rss,
estimator: "LIML".into(),
})
}
}
fn annihilate(z: &ArrayView2<f64>, w: &ArrayView2<f64>) -> StatsResult<Array2<f64>> {
let n = w.nrows();
let k = w.ncols();
let mut result = w.to_owned();
let ztz = z.t().dot(z);
let ztz_inv = cholesky_invert(&ztz.view())?;
let coef = ztz_inv.dot(&z.t().dot(w));
let projection = z.dot(&coef);
for i in 0..n {
for j in 0..k {
result[[i, j]] -= projection[[i, j]];
}
}
Ok(result)
}
fn annihilate_vec(z: &ArrayView2<f64>, v: &ArrayView1<f64>) -> StatsResult<Array1<f64>> {
let ztz = z.t().dot(z);
let ztz_inv = cholesky_invert(&ztz.view())?;
let coef = ztz_inv.dot(&z.t().dot(v));
let proj = z.dot(&coef);
Ok(v.to_owned() - proj)
}
fn min_eigenvalue(a: &ArrayView2<f64>) -> StatsResult<f64> {
let n = a.nrows();
let shift: f64 = a.diag().iter().cloned().fold(f64::NEG_INFINITY, f64::max) + 1.0;
let mut a_shifted = a.to_owned();
for i in 0..n {
a_shifted[[i, i]] -= shift;
}
let a_neg = a_shifted.mapv(|v| -v);
let mut v: Array1<f64> = Array1::ones(n);
let norm: f64 = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
v.mapv_inplace(|x| x / norm);
let a_neg_inv = cholesky_invert(&a_neg.view()).unwrap_or_else(|_| Array2::eye(n));
let mut lambda = 0.0_f64;
for _ in 0..200 {
let av = a_neg_inv.dot(&v);
let new_lambda: f64 = v.iter().zip(av.iter()).map(|(&vi, &avi)| vi * avi).sum();
let norm_av: f64 = av.iter().map(|&x| x * x).sum::<f64>().sqrt();
if norm_av < 1e-15 {
break;
}
v = av.mapv(|x| x / norm_av);
if (new_lambda - lambda).abs() < 1e-12 {
lambda = new_lambda;
break;
}
lambda = new_lambda;
}
let av_direct = a.dot(&v);
let rayleigh: f64 = v
.iter()
.zip(av_direct.iter())
.map(|(&vi, &avi)| vi * avi)
.sum();
Ok(rayleigh)
}
pub struct HausmanTest;
impl HausmanTest {
pub fn test(
y: &ArrayView1<f64>,
x_endog: &ArrayView2<f64>,
x_exog: &ArrayView2<f64>,
z_excl: &ArrayView2<f64>,
) -> StatsResult<HausmanResult> {
let k_endog = x_endog.ncols();
let k_exog = x_exog.ncols();
let k_total = k_endog + k_exog;
let n = y.len();
let mut x = Array2::<f64>::zeros((n, k_total));
for i in 0..n {
for j in 0..k_endog {
x[[i, j]] = x_endog[[i, j]];
}
for j in 0..k_exog {
x[[i, k_endog + j]] = x_exog[[i, j]];
}
}
let (beta_ols, resid_ols, xtx_inv_ols) = ols_fit(&x.view(), y)?;
let s2_ols = resid_ols.iter().map(|&r| r * r).sum::<f64>() / (n - k_total) as f64;
let vcov_ols = xtx_inv_ols.mapv(|v| v * s2_ols);
let iv_est = IVEstimator::new(false);
let iv_res = iv_est.fit(y, x_endog, x_exog, z_excl)?;
let k = k_endog; let diff: Array1<f64> = (0..k)
.map(|j| beta_ols[j] - iv_res.coefficients[j])
.collect();
let mut var_diff = Array2::<f64>::zeros((k, k));
for j in 0..k {
var_diff[[j, j]] =
(iv_res.std_errors[j] * iv_res.std_errors[j] - vcov_ols[[j, j]]).max(1e-15);
}
let var_diff_inv = cholesky_invert(&var_diff.view()).unwrap_or_else(|_| Array2::eye(k));
let h_stat: f64 = (0..k)
.map(|r| {
(0..k)
.map(|c| diff[r] * var_diff_inv[[r, c]] * diff[c])
.sum::<f64>()
})
.sum();
let p_val = chi2_p_value(h_stat.max(0.0), k);
Ok(HausmanResult {
statistic: h_stat,
p_value: p_val,
df: k,
coef_difference: diff,
endogenous_detected: p_val < 0.05,
})
}
}
pub struct WeakInstrumentTest;
impl WeakInstrumentTest {
pub fn test(
x_endog: &ArrayView2<f64>,
x_exog: &ArrayView2<f64>,
z_excl: &ArrayView2<f64>,
) -> StatsResult<WeakInstrumentResult> {
let n = x_endog.nrows();
let k_endog = x_endog.ncols();
let k_exog = x_exog.ncols();
let l_excl = z_excl.ncols();
let l_total = l_excl + k_exog;
let mut z = Array2::<f64>::zeros((n, l_total));
for i in 0..n {
for j in 0..l_excl {
z[[i, j]] = z_excl[[i, j]];
}
for j in 0..k_exog {
z[[i, l_excl + j]] = x_exog[[i, j]];
}
}
let mut f_stats = Vec::with_capacity(k_endog);
let mut pr2_vec = Vec::with_capacity(k_endog);
for j in 0..k_endog {
let xj = x_endog.column(j).to_owned();
let (f, pr2) = first_stage_diagnostics(&z.view(), &xj.view(), l_excl, k_exog)?;
f_stats.push(f);
pr2_vec.push(pr2);
}
let critical_value_10pct = 10.0_f64;
let instruments_weak: Vec<bool> =
f_stats.iter().map(|&f| f < critical_value_10pct).collect();
Ok(WeakInstrumentResult {
f_statistics: f_stats,
partial_r_squared: pr2_vec,
critical_value_10pct,
instruments_weak,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{array, Array1, Array2};
fn ones_col(n: usize) -> Array2<f64> {
Array2::ones((n, 1))
}
#[test]
fn test_2sls_just_identified() {
let n = 100_usize;
let z_vals: Array1<f64> = (0..n).map(|i| (i as f64) / 10.0).collect();
let x_vals: Array1<f64> = z_vals.mapv(|z| 0.8 * z + 0.1); let y_vals: Array1<f64> = x_vals.mapv(|x| 2.0 * x + 0.5);
let mut x_endog = Array2::<f64>::zeros((n, 1));
for i in 0..n {
x_endog[[i, 0]] = x_vals[i];
}
let x_exog = ones_col(n);
let mut z_excl = Array2::<f64>::zeros((n, 1));
for i in 0..n {
z_excl[[i, 0]] = z_vals[i];
}
let est = IVEstimator::new(false);
let res = est
.fit(
&y_vals.view(),
&x_endog.view(),
&x_exog.view(),
&z_excl.view(),
)
.expect("2SLS fit should succeed");
assert!(
(res.coefficients[0] - 2.0).abs() < 0.1,
"Expected beta≈2.0, got {}",
res.coefficients[0]
);
assert_eq!(res.estimator, "2SLS");
}
#[test]
fn test_weak_instrument_test() {
let n = 50_usize;
let z: Array1<f64> = (0..n).map(|i| i as f64).collect();
let x: Array1<f64> = z.mapv(|zi| 0.01 * zi);
let mut x_endog = Array2::<f64>::zeros((n, 1));
for i in 0..n {
x_endog[[i, 0]] = x[i];
}
let x_exog = ones_col(n);
let mut z_excl = Array2::<f64>::zeros((n, 1));
for i in 0..n {
z_excl[[i, 0]] = z[i];
}
let res = WeakInstrumentTest::test(&x_endog.view(), &x_exog.view(), &z_excl.view())
.expect("Weak instrument test should succeed");
assert_eq!(res.f_statistics.len(), 1);
assert!(res.partial_r_squared[0] >= 0.0 && res.partial_r_squared[0] <= 1.0);
}
#[test]
fn test_ols_fit_helper() {
let x = array![[1.0, 1.0], [1.0, 2.0], [1.0, 3.0], [1.0, 4.0], [1.0, 5.0]];
let y = array![2.0, 4.0, 6.0, 8.0, 10.0];
let (beta, resid, _) = ols_fit(&x.view(), &y.view()).expect("OLS should succeed");
assert!(beta[1].abs() - 2.0 < 1e-6);
assert!(resid.iter().all(|&r| r.abs() < 1e-6));
}
}