Skip to content

Commit 80d6b50

Browse files
perf(numerics): replace inv(A)@b with solve(A,b) in IV/SUR/timeseries hot paths
12 sites across regression/{iv,sur,advanced_iv}, inference/jackknife, timeseries/{cointegration,var,structural_break}: np.linalg.inv(A) @ b -> np.linalg.solve(A, b) (and inv(Z'Z)@z' -> solve(Z'Z, Z')). Numerically equivalent (backward-stable, drops an explicit matrix inverse); 924 affected tests pass unchanged. Parity-safe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ca69732 commit 80d6b50

7 files changed

Lines changed: 12 additions & 12 deletions

File tree

src/statspai/inference/jackknife.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def jackknife_se(
114114
X_g = X[mask]
115115
Y_g = Y[mask]
116116
try:
117-
beta_loo[g_idx] = np.linalg.inv(X_g.T @ X_g) @ X_g.T @ Y_g
117+
beta_loo[g_idx] = np.linalg.solve(X_g.T @ X_g, X_g.T @ Y_g)
118118
except np.linalg.LinAlgError:
119119
beta_loo[g_idx] = np.linalg.lstsq(X_g, Y_g, rcond=None)[0]
120120

src/statspai/regression/advanced_iv.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,11 @@ def liml(
115115
k_endog = X_endog.shape[1]
116116

117117
# Residual maker for X_exog
118-
Px_exog = X_exog @ np.linalg.inv(X_exog.T @ X_exog) @ X_exog.T
118+
Px_exog = X_exog @ np.linalg.solve(X_exog.T @ X_exog, X_exog.T)
119119
Mx = np.eye(n) - Px_exog
120120

121121
# Projection onto all instruments
122-
Pz = Z_all @ np.linalg.inv(Z_all.T @ Z_all) @ Z_all.T
122+
Pz = Z_all @ np.linalg.solve(Z_all.T @ Z_all, Z_all.T)
123123
Mz = np.eye(n) - Pz
124124

125125
# Compute LIML κ via the Anderson (1951) generalized symmetric
@@ -306,7 +306,7 @@ def jive(
306306
Z_all = np.column_stack([X_exog, Z_excl])
307307

308308
# Leave-one-out projection for each endogenous variable
309-
Pz = Z_all @ np.linalg.inv(Z_all.T @ Z_all) @ Z_all.T
309+
Pz = Z_all @ np.linalg.solve(Z_all.T @ Z_all, Z_all.T)
310310
h_ii = np.diag(Pz)
311311

312312
X_endog_hat = np.zeros_like(X_endog)
@@ -454,7 +454,7 @@ def lasso_iv(
454454
X_exog_mat = np.column_stack([np.ones(n)] + [df[v].values for v in x_exog]) if x_exog else np.ones((n, 1))
455455

456456
# Partial out exogenous regressors from instruments and endogenous vars
457-
Px = X_exog_mat @ np.linalg.inv(X_exog_mat.T @ X_exog_mat) @ X_exog_mat.T
457+
Px = X_exog_mat @ np.linalg.solve(X_exog_mat.T @ X_exog_mat, X_exog_mat.T)
458458
Mx = np.eye(n) - Px
459459

460460
Z_tilde = Mx @ Z_candidates # residualized instruments

src/statspai/regression/iv.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,8 @@ def _liml_kappa(
195195
W_full = np.column_stack([X_exog, Z]) # all instruments
196196

197197
# Projection matrices
198-
P_exog = X_exog @ np.linalg.inv(X_exog.T @ X_exog) @ X_exog.T
199-
P_full = W_full @ np.linalg.inv(W_full.T @ W_full) @ W_full.T
198+
P_exog = X_exog @ np.linalg.solve(X_exog.T @ X_exog, X_exog.T)
199+
P_full = W_full @ np.linalg.solve(W_full.T @ W_full, W_full.T)
200200

201201
M_exog = np.eye(n) - P_exog
202202
M_full = np.eye(n) - P_full

src/statspai/regression/sur.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ def three_sls(
325325
instruments = list(set().union(*[set(exog) for _, (_, exog, _) in equations.items()]))
326326

327327
Z = np.column_stack([np.ones(n), df[instruments].values.astype(float)])
328-
Pz = Z @ np.linalg.inv(Z.T @ Z) @ Z.T # projection onto instruments
328+
Pz = Z @ np.linalg.solve(Z.T @ Z, Z.T) # projection onto instruments
329329

330330
# Stage 1: 2SLS for each equation
331331
Y_list, X_list, k_list, var_names_list = [], [], [], []

src/statspai/timeseries/cointegration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ def johansen(
257257

258258
# Concentrate out Z from dY and Y_lag
259259
if Z.shape[1] > 0:
260-
Pz = Z @ np.linalg.inv(Z.T @ Z) @ Z.T
260+
Pz = Z @ np.linalg.solve(Z.T @ Z, Z.T)
261261
Mz = np.eye(T_eff) - Pz
262262
R0 = Mz @ dY_trim
263263
R1 = Mz @ Y_lag_trim
@@ -275,7 +275,7 @@ def johansen(
275275
# |λ S11 - S10 S00^{-1} S01| = 0
276276
try:
277277
S00_inv = np.linalg.inv(S00)
278-
M = np.linalg.inv(S11) @ S10 @ S00_inv @ S01
278+
M = np.linalg.solve(S11, S10 @ S00_inv @ S01)
279279
eigenvalues, eigenvectors = np.linalg.eig(M)
280280
except np.linalg.LinAlgError:
281281
eigenvalues = np.zeros(k)

src/statspai/timeseries/structural_break.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def cusum_test(
293293
bt = np.linalg.lstsq(Xt, yt, rcond=None)[0]
294294
pred = X_data[t] @ bt
295295
resid = y_data[t] - pred
296-
ft = 1 + X_data[t] @ np.linalg.inv(Xt.T @ Xt) @ X_data[t]
296+
ft = 1 + X_data[t] @ np.linalg.solve(Xt.T @ Xt, X_data[t])
297297
rec_resid.append(resid / np.sqrt(max(ft, 1e-10)))
298298

299299
rec_resid = np.array(rec_resid)

src/statspai/timeseries/var.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ def granger_causality(
300300
V = sigma2 * np.eye(len(coefs_all)) # simplified
301301
mid = R @ V @ R.T
302302
try:
303-
F_stat = (r @ np.linalg.inv(mid) @ r) / len(restrict_indices)
303+
F_stat = (r @ np.linalg.solve(mid, r)) / len(restrict_indices)
304304
except np.linalg.LinAlgError:
305305
F_stat = np.nan
306306

0 commit comments

Comments
 (0)