def dml_iv(W, d, z, y, kind="ridge", n_splits=3, seed=0):
y_res = y - cross_fit_predict(W, y, n_splits, seed, kind)
d_res = d - cross_fit_predict(W, d, n_splits, seed + 3, kind)
z_res = z - cross_fit_predict(W, z, n_splits, seed + 6, kind)
denom = float(np.dot(z_res, d_res))
theta = float(np.dot(z_res, y_res) / denom)
psi = z_res * (y_res - theta * d_res) / np.mean(z_res * d_res)
se = float(np.std(psi, ddof=1) / np.sqrt(len(psi)))
return theta, se
class IVLinearCATE:
"""Linear CATE from a cross-fitted IV score, for SHAP."""
def __init__(self, kind="ridge", n_splits=2, seed=0):
self.kind = kind
self.n_splits = n_splits
self.seed = seed
def fit(self, X, d, z, y, feature_names=None):
X = np.asarray(X, dtype=float)
y_res = y - cross_fit_predict(X, y, self.n_splits, self.seed, self.kind)
d_res = d - cross_fit_predict(X, d, self.n_splits, self.seed + 3, self.kind)
z_res = z - cross_fit_predict(X, z, self.n_splits, self.seed + 6, self.kind)
phi = np.column_stack([np.ones(len(X)), X])
design = z_res[:, None] * phi
# Project y_res onto the instrumented treatment interacted with phi.
# Use d_res on the left of the normal equations via 2SLS-style scaling:
# solve (Zphi' Dphi) beta = Zphi' y, with Dphi = d_res * phi.
d_design = d_res[:, None] * phi
beta, *_ = np.linalg.lstsq(design.T @ d_design, design.T @ y_res, rcond=None)
self.intercept_ = float(beta[0])
self.feature_coef_ = np.asarray(beta[1:], dtype=float)
self.feature_names_ = list(feature_names) if feature_names is not None else [f"x{j}" for j in range(X.shape[1])]
return self
def predict(self, X):
X = np.asarray(X, dtype=float)
return self.intercept_ + X @ self.feature_coef_
predict_cate = predict
print("IV helpers ready")