causarray 0.0.1__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,269 @@
1
+ import numpy as np
2
+ from sklearn.linear_model import LogisticRegression
3
+ from sklearn.ensemble import RandomForestClassifier
4
+ from causarray.gcate_glm import fit_glm
5
+ from causarray.utils import *
6
+ from causarray.utils import _filter_params
7
+ import pprint
8
+
9
+ from sklearn.model_selection import KFold, ShuffleSplit
10
+
11
+ def _get_func_ps(ps_model, **kwargs):
12
+ if ps_model=='random_forest_cv':
13
+ params_ps = _filter_params(fit_rf, kwargs)
14
+ func_ps = lambda X, Y, X_test:fit_rf_ind_ps(X, Y[:,None], X_test=X_test, **params_ps)[:,0]
15
+ elif ps_model=='logistic':
16
+ clf_ps = LogisticRegression
17
+ kwargs = {**{'fit_intercept':False, 'C':1e0, 'class_weight':'balanced', 'random_state':0}, **kwargs}
18
+ params_ps = _filter_params(clf_ps().get_params(), kwargs)
19
+ func_ps = lambda X, Y, X_test: clf_ps(**params_ps).fit(X, Y).predict_proba(X_test)[:,1]
20
+ elif ps_model=='ensemble':
21
+ params_ps_rf = _filter_params(fit_rf, kwargs)
22
+ clf_ps = LogisticRegression
23
+ kwargs = {**{'fit_intercept':False, 'C':1e0, 'class_weight':'balanced', 'random_state':0}, **kwargs}
24
+ params_ps_lr = _filter_params(clf_ps().get_params(), kwargs)
25
+ params_ps = {'params_ps_rf':params_ps_rf, 'params_ps_lr':params_ps_lr}
26
+ func_ps = lambda X, Y, X_test:(fit_rf_ind(X, Y[:,None], X_test=X_test, **params_ps_rf)[:,0] + clf_ps(**params_ps_lr).fit(X, Y).predict_proba(X_test)[:,1])/2
27
+ else:
28
+ raise ValueError('Invalid propensity score model.')
29
+
30
+ return func_ps, params_ps
31
+
32
+
33
+ def cross_fitting(
34
+ Y, A, X, X_A, family='poisson', K=1, glm_alpha=1e-4,
35
+ ps_model='logistic',
36
+ pi_hat=None, Y_hat=None, verbose=False, **kwargs):
37
+ '''
38
+ Cross-fitting for causal estimands.
39
+
40
+ Parameters
41
+ ----------
42
+ Y : array
43
+ Outcomes.
44
+ A : array
45
+ Binary treatment indicator.
46
+ X : array
47
+ Covariates.
48
+ X_A : array
49
+ Covariates for the propensity score model.
50
+ family : str, optional
51
+ The family of the generalized linear model. The default is 'poisson'.
52
+ K : int, optional
53
+ The number of folds for cross-validation. The default is 1.
54
+ glm_alpha : float, optional
55
+ The regularization parameter for the generalized linear model. The default is 1e-4.
56
+ ps_model : str, optional
57
+ The propensity score model. The default is 'logistic'.
58
+ pi_hat : array, optional
59
+ Propensity score of shape (n, a). The default is None.
60
+ Y_hat : array, optional
61
+ Estimated potential outcome of shape (n, p, a, 2). The default is None.
62
+ **kwargs : dict
63
+ Additional arguments to pass to the model.
64
+
65
+ Returns
66
+ -------
67
+ Y_hat : array
68
+ Estimated potential outcome under control.
69
+ pi_hat : array
70
+ Estimated propensity score.
71
+ '''
72
+ func_ps, params_ps = _get_func_ps(ps_model, **kwargs)
73
+ params_glm = _filter_params(fit_glm, kwargs)
74
+
75
+ if verbose:
76
+ pprint.pprint(params_ps)
77
+ pprint.pprint(params_glm)
78
+
79
+ if K>1:
80
+ # Initialize KFold cross-validator
81
+ kf = KFold(n_splits=K, random_state=0, shuffle=True)
82
+ folds = kf.split(X)
83
+ else:
84
+ folds = [(np.arange(X.shape[0]), np.arange(X.shape[0]))]
85
+
86
+ # Initialize lists to store results
87
+ fit_pi = True if pi_hat is None else False
88
+ pi_hat = np.zeros_like(A, dtype=float) if fit_pi else pi_hat
89
+ Y_hat = np.zeros((Y.shape[0],Y.shape[1],A.shape[1],2), dtype=float)
90
+
91
+ # Perform cross-fitting
92
+ for train_index, test_index in folds:
93
+ # Split data
94
+ X_train, X_test = X[train_index], X[test_index]
95
+ XA_train, XA_test = X_A[train_index], X_A[test_index]
96
+ A_train, A_test = A[train_index], A[test_index]
97
+ Y_train, Y_test = Y[train_index], Y[test_index]
98
+
99
+ if fit_pi:
100
+ i_ctrl = (np.sum(A_train, axis=1) == 0.)
101
+
102
+ pi = np.zeros_like(A_test, dtype=float)
103
+ for j in range(A.shape[1]):
104
+ i_case = (A_train[:,j] == 1.)
105
+ i_cells = i_ctrl | i_case
106
+
107
+ if ps_model=='logistic' and XA_train.shape[1]==1 and np.all(XA_train==1):
108
+ prob = np.sum(i_case)/np.sum(i_cells)
109
+ pi[A_train[:,j] == 1., j] = prob
110
+ pi[A_train[:,j] == 0., j] = 1 - prob
111
+ else:
112
+ pi[:,j] = func_ps(XA_train[i_cells], A_train[i_cells][:,j], XA_test)
113
+
114
+ # Fit GLM on training data and predict on test data
115
+ res = fit_glm(Y_train, X_train, A_train, family=family, alpha=glm_alpha,
116
+ impute=X_test, **params_glm)
117
+
118
+ # Store results
119
+ if fit_pi:
120
+ pi_hat[test_index] = pi
121
+
122
+ Y_hat[test_index,:,:,0] = res[1][0]
123
+ Y_hat[test_index,:,:,1] = res[1][1]
124
+
125
+ pi_hat = np.clip(pi_hat, 0.01, 0.99)
126
+ Y_hat = np.clip(Y_hat, None, 1e5)
127
+ return Y_hat, pi_hat
128
+
129
+
130
+
131
+
132
+
133
+ def AIPW_mean(Y, A, mu, pi, positive=False):
134
+ '''
135
+ Augmented inverse probability weighted estimator (AIPW)
136
+
137
+ Parameters
138
+ ----------
139
+ Y : array
140
+ Outcomes of shape (n, p).
141
+ A : array
142
+ Binary treatment indicator of shape (n, a, 2).
143
+ mu : array
144
+ Conditional outcome distribution estimate of shape (n, p, a, 2).
145
+ pi : array
146
+ Propensity score of shape (n, a, 2).
147
+ positive : bool, optional
148
+ Whether to restrict the pseudo-outcome to be positive.
149
+
150
+ Returns
151
+ -------
152
+ tau : array
153
+ A point estimate of the expected potential outcome of shape (p, a, 2).
154
+ pseudo_y : array
155
+ Pseudo-outcome of shape (n, p, a, 2).
156
+ '''
157
+
158
+ weight = A / pi
159
+ weight = weight[:, None, ...]
160
+ Y = Y[:, :, None, None]
161
+
162
+ pseudo_y = weight * (Y - mu) + mu
163
+
164
+ if positive:
165
+ pseudo_y = np.clip(pseudo_y, 0, None)
166
+
167
+ tau = np.mean(pseudo_y, axis=0)
168
+
169
+ return tau, pseudo_y
170
+
171
+
172
+
173
+
174
+
175
+
176
+
177
+
178
+
179
+
180
+ from joblib import Parallel, delayed
181
+ from tqdm import tqdm
182
+ from sklearn_ensemble_cv import reset_random_seeds, Ensemble, ECV
183
+ from sklearn.tree import DecisionTreeRegressor
184
+
185
+ def fit_rf(X, y, X_test=None, sample_weight=None, M=100, M_max=1000,
186
+ # fixed parameters for bagging regressor
187
+ kwargs_ensemble={'verbose':1},
188
+ # fixed parameters for decision tree
189
+ kwargs_regr={'min_samples_leaf': 3}, # 'min_samples_split': 10, 'max_features':'sqrt'
190
+ # grid search parameters
191
+ grid_regr = {'max_depth': [11]},
192
+ grid_ensemble = {'random_state': 0}, #'max_samples':np.linspace(0.25, 1., 4)
193
+ ):
194
+
195
+ # Validate integer parameters
196
+ M = int(M)
197
+ M_max = int(M_max)
198
+ # for kwargs in [kwargs_regr, kwargs_ensemble, grid_regr, grid_ensemble]:
199
+ # for param in kwargs:
200
+ # if param in ['max_depth', 'random_state', 'max_leaf_nodes'] and isinstance(kwargs[param], float):
201
+ # kwargs[param] = int(kwargs[param])
202
+
203
+ # Make sure y is 2D
204
+ y = y.reshape(-1, 1) if y.ndim == 1 else y
205
+
206
+ # Run ECV
207
+ res_ecv, info_ecv = ECV(
208
+ X, y, DecisionTreeRegressor, grid_regr, grid_ensemble,
209
+ kwargs_regr, kwargs_ensemble,
210
+ M=M, M0=M, M_max=M_max, return_df=True
211
+ )
212
+
213
+ # Replace the in-sample best parameter for 'n_estimators' with extrapolated best parameter
214
+ info_ecv['best_params_ensemble']['n_estimators'] = info_ecv['best_n_estimators_extrapolate']
215
+
216
+ # Fit the ensemble with the best CV parameters
217
+ regr = Ensemble(
218
+ estimator=DecisionTreeRegressor(**info_ecv['best_params_regr']),
219
+ **info_ecv['best_params_ensemble']).fit(X, y, sample_weight=sample_weight)
220
+
221
+ # Predict
222
+ if X_test is None:
223
+ X_test = X
224
+ return regr.predict(X_test).reshape(-1, y.shape[1])
225
+
226
+
227
+
228
+ def fit_rf_ind(X, Y, *args, **kwargs):
229
+ Y_hat = Parallel(n_jobs=-1)(delayed(fit_rf)(X, Y[:,j], *args, **kwargs)
230
+ for j in tqdm(range(Y.shape[1])))
231
+ Y_pred = np.concatenate(Y_hat, axis=-1)
232
+ return Y_pred
233
+
234
+
235
+ def fit_rf_ind_ps(X, Y, *args, **kwargs):
236
+ i_ctrl = (np.sum(Y, axis=1) == 0.)
237
+
238
+ if 'X_test' not in kwargs:
239
+ kwargs['X_test'] = X
240
+
241
+ def _fit(X, y, i_ctrl, *args, **kwargs):
242
+ i_case = (y == 1.)
243
+ i_cells = i_ctrl | i_case
244
+ sample_weight = np.ones(y.shape[0])
245
+ class_weight = len(y) / (2 * np.bincount(y.astype(int)))
246
+ for a in range(2):
247
+ sample_weight[y == a] = class_weight[a]
248
+ return fit_rf(X[i_cells], y[i_cells], sample_weight=sample_weight[i_cells], *args, **kwargs)
249
+
250
+ Y_hat = Parallel(n_jobs=-1)(delayed(_fit)(X, Y[:,j], i_ctrl, *args, **kwargs)
251
+ for j in tqdm(range(Y.shape[1])))
252
+ Y_pred = np.concatenate(Y_hat, axis=-1)
253
+
254
+ return Y_pred
255
+
256
+
257
+ def fit_rf_ind_outcome(W, Y, A, *args, **kwargs):
258
+ d = W.shape[1]
259
+ a = A.shape[1]
260
+ X = np.c_[W, A]
261
+ X_test = np.tile(np.c_[W, np.zeros_like(A)][:,None,:], (1,1+a,1))
262
+ for j in range(a):
263
+ X_test[:,1+j,d+j] = 1
264
+ X_test = X_test.reshape(-1, X_test.shape[-1])
265
+ Y_pred = fit_rf_ind(X, Y, X_test=X_test)
266
+ Y_pred = Y_pred.reshape(X.shape[0],1+a,Y.shape[1])
267
+ Yhat_1 = Y_pred[:,1:,:].transpose(0,2,1)
268
+ Yhat_0 = np.tile(Y_pred[:,0,:][:,:,None], (1,1,a))
269
+ return Yhat_0, Yhat_1
@@ -0,0 +1,201 @@
1
+ import numpy as np
2
+ import scipy as sp
3
+ import statsmodels.api as sm
4
+ from scipy.stats import norm
5
+ from statsmodels.stats.multitest import multipletests, fdrcorrection
6
+
7
+
8
+ def multiplier_bootstrap(resid, var_est, B):
9
+ '''
10
+ Multiplier bootstrap for inference.
11
+
12
+ Parameters
13
+ ----------
14
+ resid : array-like
15
+ [n, p] Residuals.
16
+ var_est : array-like
17
+ [p,] Variance of the parameter estimates.
18
+ B : int
19
+ Number of bootstrap samples.
20
+
21
+ Returns
22
+ -------
23
+ z_init : array-like
24
+ [B, p] Bootstrap statistics for each hypothesis.
25
+ '''
26
+ n, p = resid.shape
27
+ B = int(B)
28
+ z_init = np.zeros((B,p))
29
+ eta = resid / np.sqrt(n * var_est[None,:])
30
+ for b in range(B):
31
+ g = np.random.normal(size=n)
32
+ z_init[b, :] = np.sum(eta * g[:, None], axis=0)
33
+
34
+ # g = np.random.normal(size=(B,n,1))
35
+ # z_init = np.sum(resid[None, :,:] * g, axis=1) / np.sqrt(n * var_est[None,:])
36
+
37
+ return z_init
38
+
39
+
40
+ def step_down(tvalues_init, z_init, alpha):
41
+ '''
42
+ Step-down procedure for controlling FWER.
43
+
44
+ Parameters
45
+ ----------
46
+ tvalues_init : array-like
47
+ [p,] t-values for each hypothesis.
48
+ z_init : array-like
49
+ [B, p] Bootstrap statistics for each hypothesis.
50
+ alpha : float
51
+ The significance level.
52
+
53
+ Returns
54
+ -------
55
+ V : array-like
56
+ [p,] Set of discoveries.
57
+ tvalues : array-like
58
+ [p,] t-values for each hypothesis.
59
+ z : array-like
60
+ [B, p] Bootstrap statistics for each hypothesis.
61
+ '''
62
+ p = z_init.shape[1]
63
+ V = np.zeros(p,)
64
+ z = z_init.copy()
65
+ tvalues = tvalues_init.copy()
66
+ while True:
67
+ tvalues_max = np.max(np.abs(tvalues))
68
+ index_temp = np.unravel_index(np.argmax(np.abs(tvalues)), tvalues.shape)
69
+ z_max = np.max(np.abs(z), axis=1)
70
+ z_max_quan = np.quantile(z_max, 1 - alpha, method='lower')
71
+ if tvalues_max < z_max_quan or z_max_quan == 0:
72
+ break
73
+ tvalues[index_temp] = 0
74
+ z[:,index_temp] = 0
75
+ V[index_temp] = 1
76
+ return V, tvalues, z
77
+
78
+
79
+ def augmentation(V, tvalues, c):
80
+ '''
81
+ Augment the set of discoveries V.
82
+
83
+ Parameters
84
+ ----------
85
+ V : array-like
86
+ Set of discoveries.
87
+ tvalues : array-like
88
+ t-values for each hypothesis.
89
+ c : float
90
+ The exceeding level for FDP exceedance rate.
91
+
92
+ Returns
93
+ -------
94
+ V : array-like
95
+ Set of discoveries.
96
+ '''
97
+ if c>0:
98
+ size = np.sum(V)
99
+ num_add = int(np.floor(c * size / (1 - c)))
100
+ if num_add >= 1:
101
+ tvalues_sorted = np.sort(np.abs(tvalues), axis=None)[::-1]
102
+ for i in np.arange(num_add):
103
+ V[np.abs(tvalues)==tvalues_sorted[i]] = 1
104
+
105
+ return V
106
+
107
+
108
+ def fdx_control(
109
+ tau_est, var_est, tvalues_init, eta_est,
110
+ fdx, B, alpha, c,
111
+ min_var=1e-4, min_diff=0.1
112
+ ):
113
+ '''
114
+ Perform FDX control.
115
+
116
+ Parameters
117
+ ----------
118
+ tau_est : array-like
119
+ The estimated causal effect.
120
+ var_est : array-like
121
+ The estimated variance.
122
+ tvalues_init : array-like
123
+ The estimated t-values.
124
+ eta_est : array-like
125
+ The estimated influence function values.
126
+ fdx : bool
127
+ If True, perform FDX control.
128
+ B : int
129
+ Number of bootstrap samples.
130
+ alpha : float
131
+ The significance level.
132
+ c : float
133
+ Augmentation parameter.
134
+
135
+
136
+ Returns
137
+ -------
138
+ V : array-like
139
+ FDX control results, 0 for non-discoveries and 1 for discoveries.
140
+ '''
141
+ n = eta_est.shape[0]
142
+
143
+ if fdx:
144
+ id_test = var_est >= min_var
145
+ z_init = multiplier_bootstrap(eta_est, var_est, B)
146
+ z_init[:, ~id_test] = 0.
147
+ tvalues = tvalues_init.copy()
148
+ tvalues[~id_test] = 0.
149
+ V, tvalues, z = step_down(tvalues, z_init, alpha)
150
+
151
+ V[(~id_test) & (np.abs(tau_est) > min_diff)] = 1.
152
+ V = augmentation(V, tvalues, c)
153
+ else:
154
+ V = np.zeros(tvalues_init.shape[0])
155
+
156
+ return V
157
+
158
+
159
+ def bh_correction(tvalues_init):
160
+ '''
161
+ Perform BH correction.
162
+
163
+ Parameters
164
+ ----------
165
+ tvalues_init : array-like
166
+ Initial t-values.
167
+
168
+ Returns
169
+ -------
170
+ pvals : array-like
171
+ P-values.
172
+ qvals : array-like
173
+ Q-values.
174
+ pvals_adj : array-like
175
+ P-values after empirical null adjustment.
176
+ qvals_adj : array-like
177
+ Q-values after empirical null adjustment.
178
+ '''
179
+ idx = ~np.isnan(tvalues_init)
180
+
181
+ # BH correction
182
+ pvals = np.full(tvalues_init.shape, np.nan)
183
+ qvals = np.full(tvalues_init.shape, np.nan)
184
+ pvals_adj = np.full(tvalues_init.shape, np.nan)
185
+ qvals_adj = np.full(tvalues_init.shape, np.nan)
186
+
187
+ if np.sum(idx) > 0:
188
+ pvals[idx] = sp.stats.norm.sf(np.abs(tvalues_init[idx]))*2
189
+ qvals[idx] = multipletests(pvals[idx], alpha=0.05, method='fdr_bh')[1]
190
+
191
+ # BH correction with empirical null adjustment
192
+ med = np.nanmedian(tvalues_init[idx])
193
+ mad = sp.stats.median_abs_deviation(tvalues_init[idx], scale="normal", nan_policy='omit')
194
+
195
+ if mad>0:
196
+ tvalues_init_adj = (tvalues_init - med) / mad
197
+
198
+ pvals_adj[idx] = sp.stats.norm.sf(np.abs(tvalues_init_adj[idx]))*2
199
+ qvals_adj[idx] = multipletests(pvals_adj[idx], alpha=0.05, method='fdr_bh')[1]
200
+
201
+ return pvals, qvals, pvals_adj, qvals_adj