CounterfactUS 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ from .main import compute_counterfactuals
2
+
3
+ __all__ = [
4
+ "compute_counterfactuals",
5
+ ]
@@ -0,0 +1,312 @@
1
+ """
2
+ covariance_GPLSIM.py
3
+ --------------------
4
+ Computes the sandwich covariance matrix for generalized partially linear single-index
5
+ models (GPLSIM), as well as standard GAMs, GLMs, and constant models.
6
+ It extracts analytical derivatives from the piecewise polynomial spline segments
7
+ and computes the necessary gradients and Hessians to estimate the variance
8
+ of the model parameters, which is later used to compute robust CEs.
9
+ """
10
+
11
+ import numpy as np
12
+ import rpy2.robjects as ro
13
+ from scipy.stats import norm
14
+
15
+ def get_link_derivatives(family_name, link_name):
16
+ if family_name == 'gaussian' and link_name == 'identity':
17
+ def phi(eta): return eta
18
+ def phi_prime(eta): return np.ones_like(eta)
19
+ def phi_prime_prime(eta): return np.zeros_like(eta)
20
+ def psi_prime(theta): return theta
21
+ def psi_prime_prime(theta): return np.ones_like(theta)
22
+
23
+ elif family_name == 'poisson' and link_name == 'log':
24
+ def phi(eta): return eta
25
+ def phi_prime(eta): return np.ones_like(eta)
26
+ def phi_prime_prime(eta): return np.zeros_like(eta)
27
+ def psi_prime(theta): return np.exp(theta)
28
+ def psi_prime_prime(theta): return np.exp(theta)
29
+
30
+ elif family_name == 'binomial' and link_name == 'logit':
31
+ def phi(eta): return eta
32
+ def phi_prime(eta): return np.ones_like(eta)
33
+ def phi_prime_prime(eta): return np.zeros_like(eta)
34
+ def psi_prime(theta):
35
+ return 1.0 / (1.0 + np.exp(-theta))
36
+ def psi_prime_prime(theta):
37
+ p = 1.0 / (1.0 + np.exp(-theta))
38
+ return p * (1.0 - p)
39
+
40
+ elif family_name == 'binomial' and link_name == 'probit':
41
+ def phi(eta):
42
+ mu = norm.cdf(eta)
43
+ mu = np.clip(mu, 1e-15, 1 - 1e-15)
44
+ return np.log(mu / (1 - mu))
45
+ def phi_prime(eta):
46
+ mu = norm.cdf(eta)
47
+ mu = np.clip(mu, 1e-15, 1 - 1e-15)
48
+ pdf = norm.pdf(eta)
49
+ return pdf / (mu * (1 - mu))
50
+ def phi_prime_prime(eta):
51
+ mu = norm.cdf(eta)
52
+ mu = np.clip(mu, 1e-15, 1 - 1e-15)
53
+ pdf = norm.pdf(eta)
54
+ term1 = -eta * pdf * mu * (1 - mu)
55
+ term2 = (pdf**2) * (1 - 2 * mu)
56
+ return (term1 - term2) / ((mu * (1 - mu))**2)
57
+ def psi_prime(theta):
58
+ return 1.0 / (1.0 + np.exp(-theta))
59
+ def psi_prime_prime(theta):
60
+ p = 1.0 / (1.0 + np.exp(-theta))
61
+ return p * (1.0 - p)
62
+ else:
63
+ raise ValueError(f"Currently unsupported family/link combination for the computation of the sandwich formula: {family_name}/{link_name}")
64
+
65
+ return phi, phi_prime, phi_prime_prime, psi_prime, psi_prime_prime
66
+
67
+ def extract_H_and_derivatives(model_type, X, beta, n_z):
68
+ """ Extract H, H', H'' evaluations on the single-index values for gplsim or gam """
69
+ k = X.shape[0]
70
+ si = X @ beta if model_type == 'gplsim' else X[:, 0]
71
+ eps = 1e-5
72
+
73
+ ro.globalenv['si_r'] = ro.FloatVector(si)
74
+ ro.globalenv['si_plus_r'] = ro.FloatVector(si + eps)
75
+ ro.globalenv['si_minus_r'] = ro.FloatVector(si - eps)
76
+
77
+ if model_type == 'gplsim':
78
+ z_cols = max(1, n_z)
79
+ ro.globalenv['z_cols_r'] = ro.r('as.integer')(z_cols)
80
+ ro.globalenv['n_p_r'] = ro.r('as.integer')(k)
81
+
82
+ ro.r("lpmat <- predict(model_r, newdata=list(a=si_r, z=matrix(0, n_p_r, z_cols_r)), type='lpmatrix')")
83
+ ro.r("lpmat_plus <- predict(model_r, newdata=list(a=si_plus_r, z=matrix(0, n_p_r, z_cols_r)), type='lpmatrix')")
84
+ ro.r("lpmat_minus <- predict(model_r, newdata=list(a=si_minus_r, z=matrix(0, n_p_r, z_cols_r)), type='lpmatrix')")
85
+ start_col = 1 + n_z
86
+
87
+ elif model_type == 'gam':
88
+ if n_z > 0:
89
+ ro.globalenv['z_cols_r'] = ro.r('as.integer')(n_z)
90
+ ro.globalenv['n_p_r'] = ro.r('as.integer')(k)
91
+ ro.r("lpmat <- predict(model_r, newdata=list(x_sm_r=si_r, X_lin=matrix(0, n_p_r, z_cols_r)), type='lpmatrix')")
92
+ ro.r("lpmat_plus <- predict(model_r, newdata=list(x_sm_r=si_plus_r, X_lin=matrix(0, n_p_r, z_cols_r)), type='lpmatrix')")
93
+ ro.r("lpmat_minus <- predict(model_r, newdata=list(x_sm_r=si_minus_r, X_lin=matrix(0, n_p_r, z_cols_r)), type='lpmatrix')")
94
+ start_col = 1 + n_z
95
+ else:
96
+ ro.r("lpmat <- predict(model_r, newdata=list(x_sm_r=si_r), type='lpmatrix')")
97
+ ro.r("lpmat_plus <- predict(model_r, newdata=list(x_sm_r=si_plus_r), type='lpmatrix')")
98
+ ro.r("lpmat_minus <- predict(model_r, newdata=list(x_sm_r=si_minus_r), type='lpmatrix')")
99
+ start_col = 1
100
+
101
+ H = np.array(ro.r("lpmat"), dtype=np.float64, copy=True)[:, start_col:]
102
+ H_plus = np.array(ro.r("lpmat_plus"), dtype=np.float64, copy=True)[:, start_col:]
103
+ H_minus = np.array(ro.r("lpmat_minus"), dtype=np.float64, copy=True)[:, start_col:]
104
+
105
+ H_prime = (H_plus - H_minus) / (2 * eps)
106
+ H_prime_prime = (H_plus - 2 * H + H_minus) / (eps ** 2)
107
+
108
+ return H, H_prime, H_prime_prime
109
+
110
+ def get_H_analytic(model_type, knots_r, n_z, degree=3, points=500):
111
+ from .fit_GPLSIM import _fit_piecewise_from_grid
112
+
113
+ vals = np.linspace(knots_r.min(), knots_r.max(), points)
114
+
115
+ if model_type == 'gplsim':
116
+ z_cols = max(1, n_z)
117
+ ro.globalenv['z_cols_r'] = ro.r('as.integer')(z_cols)
118
+ ro.globalenv['n_p_r'] = ro.r('as.integer')(points)
119
+ ro.globalenv['si_r'] = ro.FloatVector(vals)
120
+
121
+ ro.r("lpmat <- predict(model_r, newdata=list(a=si_r, z=matrix(0, n_p_r, z_cols_r)), type='lpmatrix')")
122
+ start_col = 1 + n_z
123
+
124
+ elif model_type == 'gam':
125
+ ro.globalenv['si_r'] = ro.FloatVector(vals)
126
+ if n_z > 0:
127
+ ro.globalenv['z_cols_r'] = ro.r('as.integer')(n_z)
128
+ ro.globalenv['n_p_r'] = ro.r('as.integer')(points)
129
+ ro.r("lpmat <- predict(model_r, newdata=list(x_sm_r=si_r, X_lin=matrix(0, n_p_r, z_cols_r)), type='lpmatrix')")
130
+ start_col = 1 + n_z
131
+ else:
132
+ ro.r("lpmat <- predict(model_r, newdata=list(x_sm_r=si_r), type='lpmatrix')")
133
+ start_col = 1
134
+
135
+ H = np.array(ro.r("lpmat"), dtype=np.float64, copy=True)[:, start_col:]
136
+ l_alpha = H.shape[1]
137
+
138
+ H_analytic = {'knots': knots_r.tolist(), 'H': [], 'H_prime': [], 'H_prime_prime': []}
139
+
140
+ for j in range(l_alpha):
141
+ _, coeffs_H = _fit_piecewise_from_grid(vals, H[:, j], knots_r, degree)
142
+ H_analytic['H'].append(coeffs_H)
143
+
144
+ coeffs_prime = [np.polyder(c[::-1])[::-1].tolist() for c in coeffs_H]
145
+ coeffs_prime_prime = [np.polyder(c[::-1])[::-1].tolist() for c in coeffs_prime]
146
+
147
+ H_analytic['H_prime'].append(coeffs_prime)
148
+ H_analytic['H_prime_prime'].append(coeffs_prime_prime)
149
+
150
+ return H_analytic
151
+
152
+ def compute_sandwich_covariance(model_type, X, Z, Y, beta, gamma, family_name, link_name, include_tau=False):
153
+ """
154
+ Computes the sandwich covariance matrix universally for gplsim, gam, glm, and constant models.
155
+ """
156
+ k = len(Y)
157
+
158
+ has_alpha = model_type in ['gplsim', 'gam']
159
+ has_beta = model_type == 'gplsim'
160
+
161
+ if has_alpha:
162
+ lambda_ = float(ro.r("model_r$sp")[0])
163
+ D = np.array(ro.r("model_r$smooth[[1]]$S[[1]]"), dtype=np.float64, copy=True)
164
+ else:
165
+ lambda_ = 0.0
166
+ D = None
167
+
168
+ coefs = np.array(ro.r("model_r$coefficients"), dtype=np.float64, copy=True)
169
+ intercept = coefs[0]
170
+ n_z = Z.shape[1] if Z is not None else 0
171
+
172
+ if has_alpha:
173
+ alpha = coefs[1+n_z:]
174
+ else:
175
+ alpha = np.array([], dtype=np.float64)
176
+
177
+ if Z is not None:
178
+ Z_ext = np.column_stack([np.ones(k), np.array(Z, dtype=np.float64, copy=True)])
179
+ gamma_ext = np.concatenate([[intercept], np.array(gamma, dtype=np.float64, copy=True)])
180
+ else:
181
+ Z_ext = np.ones((k, 1), dtype=np.float64)
182
+ gamma_ext = np.array([intercept], dtype=np.float64)
183
+
184
+ if has_alpha:
185
+ H, H_prime, H_prime_prime = extract_H_and_derivatives(model_type, X, beta, n_z)
186
+ else:
187
+ H, H_prime, H_prime_prime = None, None, None
188
+
189
+ phi, phi_prime, phi_prime_prime, psi_prime, psi_prime_prime = get_link_derivatives(family_name, link_name)
190
+
191
+ l_alpha = len(alpha) if has_alpha else 0
192
+ l_beta = len(beta) if has_beta else 0
193
+ l_gamma = len(gamma_ext)
194
+
195
+ n_params = l_alpha + l_beta + l_gamma
196
+ omega_meat = np.zeros((n_params, n_params))
197
+
198
+ hess_alpha_alpha = np.zeros((l_alpha, l_alpha))
199
+ hess_alpha_beta = np.zeros((l_alpha, l_beta))
200
+ hess_alpha_gamma = np.zeros((l_alpha, l_gamma))
201
+ hess_beta_beta = np.zeros((l_beta, l_beta))
202
+ hess_beta_gamma = np.zeros((l_beta, l_gamma))
203
+ hess_gamma_gamma = np.zeros((l_gamma, l_gamma))
204
+
205
+ if has_alpha:
206
+ theta_all = np.dot(H, alpha) + np.dot(Z_ext, gamma_ext)
207
+ else:
208
+ theta_all = np.dot(Z_ext, gamma_ext)
209
+
210
+ tau = 1.0
211
+ if include_tau:
212
+ if family_name == 'gaussian':
213
+ y_pred = np.array(ro.r("predict(model_r, type='response')"))
214
+ sigma2 = np.sum((Y - y_pred)**2) / max(1, k - n_params)
215
+ tau = 1.0 / sigma2
216
+ else:
217
+ tau = 1.0
218
+
219
+ phi_all = phi(theta_all)
220
+ phi_prime_all = phi_prime(theta_all)
221
+ phi_prime_prime_all = phi_prime_prime(theta_all)
222
+ psi_prime_all = psi_prime(phi_all)
223
+ psi_prime_prime_all = psi_prime_prime(phi_all)
224
+
225
+ for i in range(k):
226
+ x_i = X[i, :] if X is not None else None
227
+ z_i = Z_ext[i, :]
228
+ y_i = Y[i]
229
+
230
+ grad_alpha_i = H[i, :] if has_alpha else np.array([])
231
+
232
+ if has_alpha and has_beta:
233
+ hess_alpha_beta_i = np.outer(H_prime[i, :], x_i)
234
+ grad_beta_i = np.dot(alpha, H_prime[i, :]) * x_i
235
+ hess_beta_beta_i = np.dot(alpha, H_prime_prime[i, :]) * np.outer(x_i, x_i)
236
+ else:
237
+ grad_beta_i = np.array([])
238
+
239
+ grad_gamma_i = z_i
240
+
241
+ phi_i = phi_all[i]
242
+ phi_prime_i = phi_prime_all[i]
243
+ phi_prime_prime_i = phi_prime_prime_all[i]
244
+ psi_prime_i = psi_prime_all[i]
245
+ psi_prime_prime_i = psi_prime_prime_all[i]
246
+
247
+ grad_L_alpha_i = (y_i - psi_prime_i) * phi_prime_i * grad_alpha_i
248
+ if has_alpha:
249
+ grad_L_alpha_i -= (lambda_ / k) * D @ alpha
250
+
251
+ grad_L_beta_i = (y_i - psi_prime_i) * phi_prime_i * grad_beta_i
252
+ grad_L_gamma_i = (y_i - psi_prime_i) * phi_prime_i * grad_gamma_i
253
+
254
+ grad_i_parts = []
255
+ if has_alpha: grad_i_parts.append(grad_L_alpha_i)
256
+ if has_beta: grad_i_parts.append(grad_L_beta_i)
257
+ grad_i_parts.append(grad_L_gamma_i)
258
+ grad_i = np.concatenate(grad_i_parts)
259
+
260
+ omega_meat += (1 / k) * np.outer(grad_i, grad_i)
261
+
262
+ term1 = -psi_prime_prime_i * (phi_prime_i ** 2)
263
+ term2 = (y_i - psi_prime_i) * phi_prime_prime_i
264
+ term3 = (y_i - psi_prime_i) * (phi_prime_i ** 2)
265
+
266
+ if has_alpha:
267
+ hess_alpha_alpha += (1 / k) * (term1 * np.outer(grad_alpha_i, grad_alpha_i) + term2 * np.outer(grad_alpha_i, grad_alpha_i))
268
+ hess_alpha_gamma += (1 / k) * (term1 * np.outer(grad_alpha_i, grad_gamma_i) + term2 * np.outer(grad_alpha_i, grad_gamma_i))
269
+ if has_beta:
270
+ hess_alpha_beta += (1 / k) * (term1 * np.outer(grad_alpha_i, grad_beta_i) + term2 * np.outer(grad_alpha_i, grad_beta_i) + term3 * hess_alpha_beta_i)
271
+
272
+ if has_beta:
273
+ hess_beta_beta += (1 / k) * (term1 * np.outer(grad_beta_i, grad_beta_i) + term2 * np.outer(grad_beta_i, grad_beta_i) + term3 * hess_beta_beta_i)
274
+ hess_beta_gamma += (1 / k) * (term1 * np.outer(grad_beta_i, grad_gamma_i) + term2 * np.outer(grad_beta_i, grad_gamma_i))
275
+
276
+ hess_gamma_gamma += (1 / k) * (term1 * np.outer(grad_gamma_i, grad_gamma_i) + term2 * np.outer(grad_gamma_i, grad_gamma_i))
277
+
278
+ if has_alpha:
279
+ hess_alpha_alpha -= (lambda_ / k) * D
280
+
281
+ # Build block hessian dynamically
282
+ blocks = []
283
+ if has_alpha:
284
+ row = [hess_alpha_alpha]
285
+ if has_beta: row.append(hess_alpha_beta)
286
+ row.append(hess_alpha_gamma)
287
+ blocks.append(row)
288
+
289
+ if has_beta:
290
+ row = [hess_alpha_beta.T] if has_alpha else []
291
+ row.append(hess_beta_beta)
292
+ row.append(hess_beta_gamma)
293
+ blocks.append(row)
294
+
295
+ # Gamma row
296
+ row = [hess_alpha_gamma.T] if has_alpha else []
297
+ if has_beta: row.append(hess_beta_gamma.T)
298
+ row.append(hess_gamma_gamma)
299
+ blocks.append(row)
300
+
301
+ hess_total = np.block(blocks)
302
+
303
+ if include_tau:
304
+ hess_total = tau * hess_total
305
+ omega_meat = tau * omega_meat
306
+
307
+ fisher_inv = np.linalg.inv(hess_total)
308
+
309
+
310
+ sandwich_estimator = (1 / k) * fisher_inv @ omega_meat @ fisher_inv.T
311
+
312
+ return sandwich_estimator