causarray 0.0.2__tar.gz → 0.0.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: causarray
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: causarray is a Python module for simultaneous causal inference with an array of outcomes.
5
5
  Author-email: Jin-Hong Du <jinhongd@andrew.cmu.com>, Maya Shen <myshen@andrew.cmu.edu>, Hansruedi Mathys <mathysh@pitt.edu>, Kathryn Roeder <jinhongd@andrew.cmu.com>
6
6
  Maintainer-email: Jin-Hong Du <jinhongd@andrew.cmu.com>
@@ -69,7 +69,7 @@ def cross_fitting(
69
69
  pi_hat : array
70
70
  Estimated propensity score.
71
71
  '''
72
- func_ps, params_ps = _get_func_ps(ps_model, verbose=verbose, **kwargs)
72
+ func_ps, params_ps = _get_func_ps(ps_model, verbose=False, **kwargs)
73
73
  params_glm = _filter_params(fit_glm, {**kwargs, 'verbose': verbose})
74
74
 
75
75
  if verbose:
@@ -86,7 +86,8 @@ def cross_fitting(
86
86
  # Initialize lists to store results
87
87
  fit_pi = True if pi_hat is None else False
88
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)
89
+ fit_Y = True if Y_hat is None else False
90
+ Y_hat = np.zeros((Y.shape[0],Y.shape[1],A.shape[1],2), dtype=float) if fit_Y else Y_hat
90
91
 
91
92
  # Perform cross-fitting
92
93
  for train_index, test_index in folds:
@@ -111,17 +112,15 @@ def cross_fitting(
111
112
  pi[A_train[:,j] == 0., j] = 1 - prob
112
113
  else:
113
114
  pi[:,j] = func_ps(XA_train[i_cells], A_train[i_cells][:,j], XA_test)
114
-
115
- if verbose: pprint.pprint('Fit outcome models...')
116
- # Fit GLM on training data and predict on test data
117
- res = fit_glm(Y_train, X_train, A_train, family=family, alpha=glm_alpha,
118
- impute=X_test, **params_glm)
119
-
120
- # Store results
121
- if fit_pi: pi_hat[test_index] = pi
122
-
123
- Y_hat[test_index,:,:,0] = res[1][0]
124
- Y_hat[test_index,:,:,1] = res[1][1]
115
+ pi_hat[test_index] = pi
116
+
117
+ if fit_Y:
118
+ if verbose: pprint.pprint('Fit outcome models...')
119
+ # Fit GLM on training data and predict on test data
120
+ res = fit_glm(Y_train, X_train, A_train, family=family, alpha=glm_alpha,
121
+ impute=X_test, **params_glm)
122
+ Y_hat[test_index,:,:,0] = res[1][0]
123
+ Y_hat[test_index,:,:,1] = res[1][1]
125
124
 
126
125
  pi_hat = np.clip(pi_hat, 0.01, 0.99)
127
126
  Y_hat = np.clip(Y_hat, None, 1e5)
@@ -5,16 +5,14 @@ from scipy.stats import norm
5
5
  from statsmodels.stats.multitest import multipletests, fdrcorrection
6
6
 
7
7
 
8
- def multiplier_bootstrap(resid, var_est, B):
8
+ def multiplier_bootstrap(eta, B):
9
9
  '''
10
10
  Multiplier bootstrap for inference.
11
11
 
12
12
  Parameters
13
13
  ----------
14
- resid : array-like
15
- [n, p] Residuals.
16
- var_est : array-like
17
- [p,] Variance of the parameter estimates.
14
+ eta : array-like
15
+ [n, p] Influence function values scaled by inversion of standard deviation.
18
16
  B : int
19
17
  Number of bootstrap samples.
20
18
 
@@ -23,17 +21,13 @@ def multiplier_bootstrap(resid, var_est, B):
23
21
  z_init : array-like
24
22
  [B, p] Bootstrap statistics for each hypothesis.
25
23
  '''
26
- n, p = resid.shape
24
+ n, p = eta.shape
27
25
  B = int(B)
28
26
  z_init = np.zeros((B,p))
29
- eta = resid / np.sqrt(n * var_est[None,:])
30
27
  for b in range(B):
31
28
  g = np.random.normal(size=n)
32
29
  z_init[b, :] = np.sum(eta * g[:, None], axis=0)
33
30
 
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
31
  return z_init
38
32
 
39
33
 
@@ -106,9 +100,9 @@ def augmentation(V, tvalues, c):
106
100
 
107
101
 
108
102
  def fdx_control(
109
- tau_est, var_est, tvalues_init, eta_est,
103
+ tau_est, tvalues_init, eta_est, std_est,
110
104
  fdx, B, alpha, c,
111
- min_var=1e-4, min_diff=0.1
105
+ min_var=1e-8, min_diff=0.1
112
106
  ):
113
107
  '''
114
108
  Perform FDX control.
@@ -117,12 +111,12 @@ def fdx_control(
117
111
  ----------
118
112
  tau_est : array-like
119
113
  The estimated causal effect.
120
- var_est : array-like
121
- The estimated variance.
122
114
  tvalues_init : array-like
123
115
  The estimated t-values.
124
116
  eta_est : array-like
125
- The estimated influence function values.
117
+ The estimated influence function values, scaled by inversion of standard deviation.
118
+ std_est : array-like
119
+ The estimated standard deviation.
126
120
  fdx : bool
127
121
  If True, perform FDX control.
128
122
  B : int
@@ -141,8 +135,8 @@ def fdx_control(
141
135
  n = eta_est.shape[0]
142
136
 
143
137
  if fdx:
144
- id_test = var_est >= min_var
145
- z_init = multiplier_bootstrap(eta_est, var_est, B)
138
+ id_test = std_est >= min_var
139
+ z_init = multiplier_bootstrap(eta_est / std_est[None,:], B)
146
140
  z_init[:, ~id_test] = 0.
147
141
  tvalues = tvalues_init.copy()
148
142
  tvalues[~id_test] = 0.
@@ -11,7 +11,7 @@ from causarray.utils import reset_random_seeds, pprint, tqdm, comp_size_factor,
11
11
  def compute_causal_estimand(
12
12
  estimand,
13
13
  Y, W, A, W_A=None, family='nb', offset=False,
14
- Y_hat=None, pi_hat=None, eps_var=1e-3,
14
+ Y_hat=None, pi_hat=None, mask=None,
15
15
  fdx=False, fdx_B=1000, fdx_alpha=0.05, fdx_c=0.1,
16
16
  verbose=False, random_state=0, **kwargs):
17
17
  '''
@@ -39,6 +39,10 @@ def compute_causal_estimand(
39
39
  Predicted outcomes under treatment of shape (n, p, a, 2).
40
40
  pi_hat : array, optional
41
41
  Predicted propensity scores of shape (n, a).
42
+ mask : array, optional
43
+ Boolean mask of shape (n, a) for the treatment, indicating which samples are used for
44
+ the estimation of the estimand. This does not affect the estimation of pseudo-outcomes
45
+ and propensity scores.
42
46
 
43
47
  fdx : bool
44
48
  Whether to use FDX control, P(FDP > c) < alpha.
@@ -87,6 +91,12 @@ def compute_causal_estimand(
87
91
  W_A = W
88
92
  elif isinstance(W_A, pd.DataFrame):
89
93
  W_A = W_A.values
94
+
95
+ if mask is not None:
96
+ mask = np.array(mask).astype(bool)
97
+ if len(mask.shape) == 1: mask = mask.reshape(-1,1)
98
+ if mask.shape != A.shape:
99
+ raise ValueError('Mask must have the same shape as the treatment matrix')
90
100
 
91
101
 
92
102
  if verbose:
@@ -117,25 +127,25 @@ def compute_causal_estimand(
117
127
  # normalize the influence function values
118
128
  etas /= size_factors[:,None,None,None]
119
129
 
120
-
121
- i_ctrl = (np.sum(A, axis=1) == 0.)
122
-
123
130
  res = []
124
131
  iters = range(A.shape[1]) if A.shape[1]==1 else tqdm(range(A.shape[1]))
125
132
  for j in iters:
126
- i_case = (A[:,j] == 1.)
127
- i_cells = i_ctrl | i_case
128
- n_cells = np.sum(i_cells)
129
- eta_est, tau_est, var_est = estimand(etas[i_cells,:,j], **kwargs)
133
+ if mask is not None:
134
+ i_cells = mask[:, j]
135
+ else:
136
+ i_ctrl = (np.sum(A, axis=1) == 0.)
137
+ i_case = (A[:,j] == 1.)
138
+ i_cells = i_ctrl | i_case
139
+ eta_est, tau_est, var_est = estimand(etas[i_cells,:,j], A[i_cells,j], **kwargs)
130
140
 
131
- std_est = np.sqrt((var_est + eps_var)/ n_cells)
141
+ std_est = np.sqrt(var_est)
132
142
  tvalues_init = tau_est / std_est
133
143
 
134
144
  # Multiple testing procedure
135
- V = fdx_control(tau_est, var_est, tvalues_init, eta_est, fdx, fdx_B, fdx_alpha, fdx_c)
145
+ V = fdx_control(tau_est, tvalues_init, eta_est, std_est, fdx, fdx_B, fdx_alpha, fdx_c)
136
146
 
137
147
  # BH correction
138
- tvalues_init[np.isinf(var_est)] = np.nan
148
+ tvalues_init[np.isinf(std_est)] = np.nan
139
149
  pvals, qvals, pvals_adj, qvals_adj = bh_correction(tvalues_init)
140
150
 
141
151
  df_res = pd.DataFrame({
@@ -159,7 +169,7 @@ def compute_causal_estimand(
159
169
 
160
170
  def LFC(
161
171
  Y, W, A, W_A=None, family='nb', offset=False,
162
- Y_hat=None, pi_hat=None, cross_est=False,
172
+ Y_hat=None, pi_hat=None, cross_est=False, mask=None, usevar='pooled',
163
173
  thres_min=1e-4, thres_diff=1e-6, eps_var=1e-3,
164
174
  fdx=False, fdx_alpha=0.05, fdx_c=0.1,
165
175
  verbose=False, **kwargs):
@@ -187,12 +197,18 @@ def LFC(
187
197
  Predicted propensity scores of shape (n, a).
188
198
  cross_est : bool
189
199
  Whether to use cross-estimation.
200
+ mask : array, optional
201
+ Boolean mask of shape (n, a) for the treatment, indicating which samples are used for
202
+ the estimation of the estimand. This does not affect the estimation of pseudo-outcomes
203
+ and propensity scores.
190
204
 
191
205
  thres_min : float
192
206
  The minimum threshold for the treatment effect.
193
207
  thres_diff : float
194
208
  The minimum threshold for the difference in treatment effect.
195
-
209
+ eps_var : float
210
+ The minimum threshold for the variance of treatment.
211
+
196
212
  fdx : bool
197
213
  Whether to use FDX control, P(FDP > c) < alpha.
198
214
  fdx_alpha : float
@@ -211,7 +227,7 @@ def LFC(
211
227
  Dataframe of test results.
212
228
  '''
213
229
 
214
- def estimand(etas, **kwargs):
230
+ def estimand(etas, A, **kwargs):
215
231
  eta_0, eta_1 = etas[..., 0], etas[..., 1]
216
232
  tau_0, tau_1 = np.mean(eta_0, axis=0), np.mean(eta_1, axis=0)
217
233
 
@@ -219,7 +235,18 @@ def LFC(
219
235
  tau_0 = np.clip(tau_0, thres_diff, None)
220
236
  tau_est = np.log(tau_1/tau_0)
221
237
  eta_est = eta_1 / tau_1[None,:] - eta_0 / tau_0[None,:]
222
- var_est = np.var(eta_est, axis=0, ddof=1)
238
+
239
+ if usevar == 'pooled':
240
+ var_est = (np.var(eta_est, axis=0, ddof=1) + eps_var) / eta_est.shape[0]
241
+ elif usevar == 'unequal':
242
+ # Estimate the variance using Welch's t-test
243
+ var_0 = np.var(eta_est[A==0], axis=0, ddof=1)
244
+ var_1 = np.var(eta_est[A==1], axis=0, ddof=1)
245
+ n_0 = np.sum(A==0)
246
+ n_1 = np.sum(A==1)
247
+ var_est = (var_0 + eps_var) / n_0 + (var_1 + eps_var) / n_1
248
+ else:
249
+ raise ValueError('usevar must be either "pooled" or "unequal"')
223
250
 
224
251
  # filter out low-expressed genes
225
252
  idx = (np.maximum(tau_0,tau_1)<thres_min) & ((tau_1-tau_0)<thres_diff)
@@ -229,7 +256,7 @@ def LFC(
229
256
 
230
257
  return compute_causal_estimand(
231
258
  estimand, Y, W, A, W_A, family, offset,
232
- Y_hat=Y_hat, pi_hat=pi_hat,
259
+ Y_hat=Y_hat, pi_hat=pi_hat, mask=mask,
233
260
  fdx=fdx, fdx_alpha=fdx_alpha, fdx_c=fdx_c, verbose=verbose, **kwargs)
234
261
 
235
262
 
@@ -0,0 +1 @@
1
+ __version__ = "0.0.3"
@@ -195,8 +195,8 @@ def estimate_r(Y, X, A, r_max, c=1.,
195
195
  df_r : DataFrame
196
196
  Results of the number of latent factors.
197
197
  '''
198
- X = np.hstack((X, A))
199
198
  a, d = A.shape[1], X.shape[1]
199
+ X = np.hstack((X, A))
200
200
  n, p = Y.shape
201
201
 
202
202
  Y, kwargs_glm, _ = _check_input(Y, X, family, disp_glm, disp_family, offset, None, **kwargs)
@@ -210,22 +210,31 @@ def estimate_r(Y, X, A, r_max, c=1.,
210
210
  r_list = np.arange(1, int(r_max)+1)
211
211
  else:
212
212
  r_list = np.array(r_max, dtype=int)
213
-
214
- for r in r_list:
215
- res_1, res_2 = estimate(Y, X, r, a,
216
- 0, kwargs_glm, kwargs_ls_1, kwargs_es_1, kwargs_ls_2, kwargs_es_2, **kwargs)
217
- A01, A02, A1, A2 = res_1['X_U'], res_1['B_Gamma'], res_2['X_U'], res_2['B_Gamma']
218
-
219
- logh = log_h(Y, family, nuisance)
220
-
221
- if r==1:
222
- ll = 2 * (
223
- nll(Y, A01, A02, family, nuisance, size_factor) / p
224
- - np.sum(logh) / (n*p) )
225
- nu = (d+a) * np.maximum(n,p) * np.log(n * p / np.maximum(n,p)) / (n*p)
226
- jic = ll + c * nu
227
- res.append([0, ll, nu, jic])
228
-
213
+ r_max = np.max(r_list)
214
+
215
+ # Estimate the residual deviance
216
+ res_glm = fit_glm(Y, X, offset=np.log(size_factor[:,0]), family=family, disp_glm=nuisance[0], maxiter=100, verbose=False)
217
+ u, s, vt = svds(res_glm[-1], k=r_max)
218
+ if u.shape[1]<r_max:
219
+ raise ValueError(f'The number of latent factors is larger than the rank of deviance residuals ({u.shape[1]}). Try to decrease the value of r.')
220
+ Q, _ = sp.linalg.qr(X, mode='economic')
221
+ P1 = np.identity(n) - Q @ Q.T
222
+ P1 = P1.astype(type_f)
223
+ A1 = np.c_[X, P1 @ u]
224
+
225
+ logh = log_h(Y, family, nuisance)
226
+ ll = 2 * (
227
+ nll(Y, X, res_glm[0], family, nuisance, size_factor) / p
228
+ - np.sum(logh) / (n*p) )
229
+ nu = (d+a) * np.maximum(n,p) * np.log(n * p / np.maximum(n,p)) / (n*p)
230
+ jic = ll + c * nu
231
+ res.append([0, ll, nu, jic])
232
+
233
+ for r in r_list[::-1]:
234
+ _, res_2 = estimate(Y, X, r, a,
235
+ 0, kwargs_glm, kwargs_ls_1, kwargs_es_1, kwargs_ls_2, kwargs_es_2, A=A1[:,:d+a+r], **kwargs)
236
+ A1, A2 = res_2['X_U'], res_2['B_Gamma']
237
+
229
238
  ll = 2 * (
230
239
  nll(Y, A1, A2, family, nuisance, size_factor) / p
231
240
  - np.sum(logh) / (n*p) )
@@ -233,5 +242,5 @@ def estimate_r(Y, X, A, r_max, c=1.,
233
242
  jic = ll + c * nu
234
243
  res.append([r, ll, nu, jic])
235
244
 
236
- df_r = pd.DataFrame(res, columns=['r', 'deviance', 'nu', 'JIC'])
245
+ df_r = pd.DataFrame(res, columns=['r', 'deviance', 'nu', 'JIC']).sort_values(by='r')
237
246
  return df_r
@@ -156,8 +156,7 @@ def fit_glm(Y, X, A=None, family='gaussian', disp_family='poisson',
156
156
 
157
157
 
158
158
  results = Parallel(n_jobs=n_jobs)(delayed(fit_model)(
159
- j, Y, X, offsets, family, disp_glm, impute, alpha) for j in tqdm(range(Y.shape[1])))
160
- pprint.pprint('Fitting GLM done.')
159
+ j, Y, X, offsets, family, disp_glm, impute, alpha) for j in tqdm(range(Y.shape[1]), disable=not verbose))
161
160
  if verbose: pprint.pprint('Fitting GLM done.')
162
161
 
163
162
  B, Yhat_0, Yhat_1, resid_deviance = zip(*results)
@@ -208,19 +208,24 @@ def alter_min(
208
208
  a = d
209
209
 
210
210
  # initialization for Theta = A @ B^T
211
- if A is None or B is None:
211
+ if A is None:
212
212
  if verbose:
213
213
  pprint.pprint('Estimating initial latent variables with GLMs...')
214
- res_glm = fit_glm(Y, X, offset=np.log(size_factor[:,0]), family=family, disp_glm=nuisance[0], maxiter=100)
214
+ res_glm = fit_glm(Y, X, offset=np.log(size_factor[:,0]), family=family, disp_glm=nuisance[0], maxiter=100, verbose=verbose)
215
215
  u, s, vt = svds(res_glm[-1], k=r)
216
216
 
217
217
  if u.shape[1]<r:
218
218
  raise ValueError(f'The number of latent factors is larger than the rank of deviance residuals ({u.shape[1]}). Try to decrease the value of r.')
219
+
220
+ A = np.c_[X, P1 @ u]
221
+ else:
222
+ assert A.shape[1] == d+r
219
223
 
224
+ if B is None:
220
225
  if verbose:
221
226
  pprint.pprint('Estimating initial coefficients with GLMs...')
222
- A = np.c_[X, P1 @ u]
223
- B = fit_glm(Y, A, offset=np.log(size_factor[:,0]), family=family, disp_glm=nuisance[0], maxiter=100)[0]
227
+
228
+ B = fit_glm(Y, A, offset=np.log(size_factor[:,0]), family=family, disp_glm=nuisance[0], maxiter=100, verbose=verbose)[0]
224
229
 
225
230
  E = A[:, -r:] @ B[:, -r:].T
226
231
  u, s, vh = sp.sparse.linalg.svds(E, k=r)
@@ -228,19 +233,10 @@ def alter_min(
228
233
  B[:, d:] = vh.T * s[None,:]**(1/2)
229
234
  del E, u, s, vh
230
235
 
231
- # if offset==1:
232
- # scale = np.sqrt(np.median(np.abs(X[:,0])))
233
- # B[:, :offset] = scale
234
- # A[:, :offset] /= scale
235
236
 
236
237
  if P2 is not None:
237
238
  P2 = P2.astype(type_f)
238
- # E = A[:,d-a:] @ B[:,d-a:].T @ (np.identity(p) - P2)
239
- # u, s, vh = sp.sparse.linalg.svds(E, k=r)
240
239
  B[:, d-a:d] = P2 @ B[:, d-a:d]
241
- # A[:, d:] = u * s[None,:]**(1/2)
242
- # B[:, d:] = vh.T * s[None,:]**(1/2)
243
- # del E, u, s, vh
244
240
 
245
241
 
246
242
  Y = Y.astype(type_f)
@@ -265,10 +261,10 @@ def alter_min(
265
261
  kwargs_ls['alpha'] = kwargs_ls['alpha']
266
262
  if verbose:
267
263
  pprint.pprint({'kwargs_glm':kwargs_glm,'kwargs_ls':kwargs_ls,'kwargs_es':kwargs_es}, compact=True)
268
-
264
+ pprint.pprint(f'Fitting GCATE (step {1 if P1 is None else 2})...')
269
265
  hist = [func_val_pre]
270
266
  es = Early_Stopping(**kwargs_es)
271
- with tqdm(np.arange(kwargs_es['max_iters'])) as pbar:
267
+ with tqdm(np.arange(kwargs_es['max_iters']), disable=not verbose) as pbar:
272
268
  for t in pbar:
273
269
  func_val, A, B = update(
274
270
  Y, A, B, d, weights, P1, P2,
@@ -281,7 +277,7 @@ def alter_min(
281
277
  pprint.pprint('Encountered large or infinity values. Try to decrease the value of C for the norm constraints.')
282
278
  break
283
279
  elif es(func_val):
284
- pbar.set_postfix_str('Early stopped.' + es.info)
280
+ pbar.set_postfix_str('Early stopped. ' + es.info)
285
281
  pbar.close()
286
282
  break
287
283
  else:
@@ -50,15 +50,16 @@ def prep_causarray_data(Y, A, X=None, X_A=None, intercept=True):
50
50
  if not isinstance(A, pd.DataFrame):
51
51
  A = np.asarray(A)
52
52
 
53
- X = np.zeros((Y.shape[0], 0)) if X is None else np.asarray(X)
54
- intercept_col = np.ones((X.shape[0], 1)) if intercept else np.empty((X.shape[0], 0))
55
- X = np.hstack((intercept_col, X))
56
-
53
+ X = np.zeros((Y.shape[0], 0)) if X is None else np.asarray(X)
57
54
  X_A = X if X_A is None else np.asarray(X_A)
58
55
  loglibsize = np.log2(np.sum(np.asarray(Y), axis=1))
59
56
  loglibsize = (loglibsize - np.mean(loglibsize)) / np.std(loglibsize, ddof=1)
60
57
  X_A = np.hstack((X_A, loglibsize[:, None]))
61
58
 
59
+ intercept_col = np.ones((X.shape[0], 1)) if intercept else np.empty((X.shape[0], 0))
60
+ X = np.hstack((intercept_col, X))
61
+ X_A = np.hstack((intercept_col, X_A))
62
+
62
63
  return Y, A, X, X_A
63
64
 
64
65
 
@@ -1 +0,0 @@
1
- __version__ = "0.0.2"
File without changes
File without changes
File without changes
File without changes