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.
- counterfactus/__init__.py +5 -0
- counterfactus/covariance_GPLSIM.py +312 -0
- counterfactus/fit_GPLSIM.py +412 -0
- counterfactus/main.py +1047 -0
- counterfactus/solver.py +587 -0
- counterfactus-0.1.0.dist-info/METADATA +142 -0
- counterfactus-0.1.0.dist-info/RECORD +10 -0
- counterfactus-0.1.0.dist-info/WHEEL +5 -0
- counterfactus-0.1.0.dist-info/licenses/LICENSE +21 -0
- counterfactus-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|