causarray 0.0.3__tar.gz → 0.0.5__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.
@@ -160,4 +160,5 @@ cython_debug/
160
160
  #.idea/
161
161
 
162
162
 
163
- test*
163
+ test*
164
+ causarray/___*.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: causarray
3
- Version: 0.0.3
3
+ Version: 0.0.5
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>
@@ -1,9 +1,12 @@
1
1
  import numpy as np
2
2
  from sklearn.linear_model import LogisticRegression
3
- from sklearn.ensemble import RandomForestClassifier
3
+ from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
4
+ from sklearn_ensemble_cv import reset_random_seeds, Ensemble, ECV
4
5
  from causarray.gcate_glm import fit_glm
5
6
  from causarray.utils import *
6
7
  from causarray.utils import _filter_params
8
+ from joblib import Parallel, delayed
9
+ from tqdm import tqdm
7
10
  import pprint
8
11
 
9
12
  from sklearn.model_selection import KFold, ShuffleSplit
@@ -33,7 +36,7 @@ def _get_func_ps(ps_model, **kwargs):
33
36
  def cross_fitting(
34
37
  Y, A, X, X_A, family='poisson', K=1, glm_alpha=1e-4,
35
38
  ps_model='logistic',
36
- pi_hat=None, Y_hat=None, verbose=False, **kwargs):
39
+ Y_hat=None, pi_hat=None, mask=None, verbose=False, **kwargs):
37
40
  '''
38
41
  Cross-fitting for causal estimands.
39
42
 
@@ -55,10 +58,16 @@ def cross_fitting(
55
58
  The regularization parameter for the generalized linear model. The default is 1e-4.
56
59
  ps_model : str, optional
57
60
  The propensity score model. The default is 'logistic'.
58
- pi_hat : array, optional
59
- Propensity score of shape (n, a). The default is None.
61
+
60
62
  Y_hat : array, optional
61
63
  Estimated potential outcome of shape (n, p, a, 2). The default is None.
64
+ pi_hat : array, optional
65
+ Propensity score of shape (n, a). The default is None.
66
+ mask : array, optional
67
+ Boolean mask of shape (n, a) for the treatment, indicating which samples are used for
68
+ the estimation of the estimand. This does not affect the estimation of pseudo-outcomes
69
+ and propensity scores.
70
+
62
71
  **kwargs : dict
63
72
  Additional arguments to pass to the model.
64
73
 
@@ -76,10 +85,15 @@ def cross_fitting(
76
85
  pprint.pprint(params_ps)
77
86
  pprint.pprint(params_glm)
78
87
 
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)
88
+ if K > 1:
89
+ n_samples = X.shape[0]
90
+ if K >= n_samples:
91
+ # Use Leave-One-Out Cross-Validation
92
+ folds = [([i for i in range(n_samples) if i != j], [j]) for j in range(n_samples)]
93
+ else:
94
+ # Initialize KFold cross-validator
95
+ kf = KFold(n_splits=int(K), random_state=0, shuffle=True)
96
+ folds = kf.split(X)
83
97
  else:
84
98
  folds = [(np.arange(X.shape[0]), np.arange(X.shape[0]))]
85
99
 
@@ -89,6 +103,16 @@ def cross_fitting(
89
103
  fit_Y = True if Y_hat is None else False
90
104
  Y_hat = np.zeros((Y.shape[0],Y.shape[1],A.shape[1],2), dtype=float) if fit_Y else Y_hat
91
105
 
106
+ # perform ECV at once
107
+ if fit_pi and ps_model == 'random_forest_cv':
108
+ info_ecv = run_ecv(X_A, A, **params_ps)
109
+ func_ps, params_ps = _get_func_ps(ps_model, verbose=False, ecv=False,
110
+ kwargs_ensemble=info_ecv['best_params_ensemble'], kwargs_regr=info_ecv['best_params_regr'])
111
+ pprint.pprint('Best parameters for the regression model:')
112
+ pprint.pprint(info_ecv['best_params_regr'])
113
+ pprint.pprint('Best parameters for the ensemble model:')
114
+ pprint.pprint(info_ecv['best_params_ensemble'])
115
+
92
116
  # Perform cross-fitting
93
117
  for train_index, test_index in folds:
94
118
  # Split data
@@ -104,7 +128,12 @@ def cross_fitting(
104
128
  pi = np.zeros_like(A_test, dtype=float)
105
129
  for j in range(A.shape[1]):
106
130
  i_case = (A_train[:,j] == 1.)
107
- i_cells = i_ctrl | i_case
131
+
132
+ if mask is not None:
133
+ i_cells = mask[:, j]
134
+ else:
135
+ i_ctrl = (np.sum(A_train, axis=1) == 0.)
136
+ i_cells = i_ctrl | i_case
108
137
 
109
138
  if ps_model=='logistic' and XA_train.shape[1]==1 and np.all(XA_train==1):
110
139
  prob = np.sum(i_case)/np.sum(i_cells)
@@ -167,8 +196,6 @@ def AIPW_mean(Y, A, mu, pi, positive=False):
167
196
  tau = np.mean(pseudo_y, axis=0)
168
197
 
169
198
  return tau, pseudo_y
170
-
171
-
172
199
 
173
200
 
174
201
 
@@ -177,51 +204,89 @@ def AIPW_mean(Y, A, mu, pi, positive=False):
177
204
 
178
205
 
179
206
 
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,
207
+ def run_ecv(
208
+ X, y, M=200, M_max=1000,
186
209
  # fixed parameters for bagging regressor
187
- kwargs_ensemble={'verbose':1},
210
+ kwargs_ensemble={},
188
211
  # fixed parameters for decision tree
189
- kwargs_regr={'min_samples_leaf': 3}, # 'min_samples_split': 10, 'max_features':'sqrt'
212
+ kwargs_regr={},
190
213
  # grid search parameters
191
- grid_regr = {'max_depth': [11]},
192
- grid_ensemble = {'random_state': 0}, #'max_samples':np.linspace(0.25, 1., 4)
193
- ):
214
+ grid_regr={},
215
+ grid_ensemble={}
216
+ ):
217
+ """
218
+ Runs Ensemble Cross-Validation (ECV) to find the best hyperparameters.
219
+ """
220
+ kwargs_ensemble = {**{'verbose': 1, 'bootstrap': True}, **kwargs_ensemble}
221
+ kwargs_regr = {**{'min_samples_split': 20, 'min_samples_leaf': 10, 'max_features': 'sqrt', 'ccp_alpha': 0.02, 'class_weight': 'balanced'}, **kwargs_regr}
222
+ grid_regr = {**{'max_depth': [3, 5, 7]}, **grid_regr}
223
+ grid_ensemble = {**{'random_state': 0, 'max_samples': [0.4, 0.6, 0.8, 1.]}, **grid_ensemble}
194
224
 
195
225
  # Validate integer parameters
196
226
  M = int(M)
197
227
  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
228
 
203
229
  # Make sure y is 2D
204
230
  y = y.reshape(-1, 1) if y.ndim == 1 else y
205
231
 
206
232
  # Run ECV
207
- res_ecv, info_ecv = ECV(
208
- X, y, DecisionTreeRegressor, grid_regr, grid_ensemble,
209
- kwargs_regr, kwargs_ensemble,
233
+ _, info_ecv = ECV(
234
+ X, y, DecisionTreeClassifier, grid_regr, grid_ensemble,
235
+ kwargs_regr, kwargs_ensemble,
210
236
  M=M, M0=M, M_max=M_max, return_df=True
211
237
  )
212
238
 
213
239
  # Replace the in-sample best parameter for 'n_estimators' with extrapolated best parameter
214
240
  info_ecv['best_params_ensemble']['n_estimators'] = info_ecv['best_n_estimators_extrapolate']
215
241
 
242
+ return info_ecv
243
+
244
+
245
+ def fit_rf(
246
+ X, y, X_test=None, M=100, M_max=1000, ecv=True,
247
+ # fixed parameters for bagging regressor
248
+ kwargs_ensemble={},
249
+ # fixed parameters for decision tree
250
+ kwargs_regr={},
251
+ # grid search parameters
252
+ grid_regr={},
253
+ grid_ensemble={}
254
+ ):
255
+ """
256
+ Fits a Random Forest model using parameters found by ECV.
257
+ """
258
+
259
+ kwargs_ensemble = {**{'verbose': 1, 'bootstrap': True}, **kwargs_ensemble}
260
+ kwargs_regr = {**{'min_samples_split': 20, 'min_samples_leaf': 10, 'max_features': 'sqrt', 'ccp_alpha': 0.02, 'class_weight': 'balanced'}, **kwargs_regr}
261
+ grid_regr = {**{'max_depth': [3, 5, 7]}, **grid_regr}
262
+ grid_ensemble = {**{'random_state': 0, 'max_samples': [0.4, 0.6, 0.8, 1.]}, **grid_ensemble}
263
+
264
+ # Make sure y is 2D
265
+ y_2d = y.reshape(-1, 1) if y.ndim == 1 else y
266
+
267
+ if ecv:
268
+ # Get best parameters from ECV
269
+ info_ecv = run_ecv(
270
+ X, y_2d, M=M, M_max=M_max,
271
+ kwargs_ensemble=kwargs_ensemble,
272
+ kwargs_regr=kwargs_regr,
273
+ grid_regr=grid_regr,
274
+ grid_ensemble=grid_ensemble
275
+ )
276
+ params_regr = info_ecv['best_params_regr']
277
+ params_ensemble = info_ecv['best_params_ensemble']
278
+ else:
279
+ params_regr = kwargs_regr
280
+ params_ensemble = kwargs_ensemble
281
+
216
282
  # Fit the ensemble with the best CV parameters
217
283
  regr = Ensemble(
218
- estimator=DecisionTreeRegressor(**info_ecv['best_params_regr']),
219
- **info_ecv['best_params_ensemble']).fit(X, y, sample_weight=sample_weight)
220
-
284
+ estimator=DecisionTreeClassifier(**params_regr), **params_ensemble).fit(X, y_2d)
285
+
221
286
  # Predict
222
287
  if X_test is None:
223
288
  X_test = X
224
- return regr.predict(X_test).reshape(-1, y.shape[1])
289
+ return regr.predict(X_test).reshape(-1, y_2d.shape[1])
225
290
 
226
291
 
227
292
 
@@ -241,11 +306,7 @@ def fit_rf_ind_ps(X, Y, *args, **kwargs):
241
306
  def _fit(X, y, i_ctrl, *args, **kwargs):
242
307
  i_case = (y == 1.)
243
308
  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)
309
+ return fit_rf(X[i_cells], y[i_cells], *args, **kwargs)
249
310
 
250
311
  Y_hat = Parallel(n_jobs=-1)(delayed(_fit)(X, Y[:,j], i_ctrl, *args, **kwargs)
251
312
  for j in tqdm(range(Y.shape[1])))
@@ -65,15 +65,13 @@ def compute_causal_estimand(
65
65
  '''
66
66
  reset_random_seeds(random_state)
67
67
 
68
- kwargs = {k:v for k,v in kwargs.items() if k not in
69
- ['kwargs_ls_1', 'kwargs_ls_2', 'kwargs_es_1', 'kwargs_es_2', 'c1', 'num_d']
70
- }
71
-
68
+ # check the input data
72
69
  if isinstance(Y, pd.DataFrame):
73
70
  gene_names = Y.columns
74
71
  Y = Y.values
75
72
  else:
76
73
  gene_names = range(Y.shape[1])
74
+ Y = Y.astype('float')
77
75
  n, p = Y.shape
78
76
 
79
77
  if len(A.shape) == 1:
@@ -97,7 +95,10 @@ def compute_causal_estimand(
97
95
  if len(mask.shape) == 1: mask = mask.reshape(-1,1)
98
96
  if mask.shape != A.shape:
99
97
  raise ValueError('Mask must have the same shape as the treatment matrix')
100
-
98
+
99
+ kwargs = {k:v for k,v in kwargs.items() if k not in
100
+ ['kwargs_ls_1', 'kwargs_ls_2', 'kwargs_es_1', 'kwargs_es_2', 'c1', 'num_d']
101
+ }
101
102
 
102
103
  if verbose:
103
104
  d_A = W_A.shape[1]
@@ -113,10 +114,9 @@ def compute_causal_estimand(
113
114
  else:
114
115
  offset = None
115
116
  size_factors = np.ones(n)
116
-
117
- Y = Y.astype('float')
117
+
118
118
  Y_hat, pi_hat = cross_fitting(Y, A, W, W_A, family=family, offset=offset,
119
- Y_hat=Y_hat, pi_hat=pi_hat, random_state=random_state, verbose=verbose, **kwargs)
119
+ Y_hat=Y_hat, pi_hat=pi_hat, mask=mask, random_state=random_state, verbose=verbose, **kwargs)
120
120
  pi_hat = pi_hat.reshape(*A.shape)
121
121
 
122
122
  if verbose: pprint.pprint('Estimating AIPW mean...')
@@ -170,7 +170,7 @@ def compute_causal_estimand(
170
170
  def LFC(
171
171
  Y, W, A, W_A=None, family='nb', offset=False,
172
172
  Y_hat=None, pi_hat=None, cross_est=False, mask=None, usevar='pooled',
173
- thres_min=1e-4, thres_diff=1e-6, eps_var=1e-3,
173
+ thres_min=1e-2, thres_diff=1e-2, eps_var=1e-4,
174
174
  fdx=False, fdx_alpha=0.05, fdx_c=0.1,
175
175
  verbose=False, **kwargs):
176
176
  '''
@@ -244,12 +244,12 @@ def LFC(
244
244
  var_1 = np.var(eta_est[A==1], axis=0, ddof=1)
245
245
  n_0 = np.sum(A==0)
246
246
  n_1 = np.sum(A==1)
247
- var_est = (var_0 + eps_var) / n_0 + (var_1 + eps_var) / n_1
247
+ var_est = ((var_0 + eps_var) / n_0 + (var_1 + eps_var) / n_1) / 2
248
248
  else:
249
249
  raise ValueError('usevar must be either "pooled" or "unequal"')
250
250
 
251
251
  # filter out low-expressed genes
252
- idx = (np.maximum(tau_0,tau_1)<thres_min) & ((tau_1-tau_0)<thres_diff)
252
+ idx = (np.maximum(np.abs(tau_0),np.abs(tau_1))<thres_min) | (np.abs(tau_1-tau_0)<thres_diff)
253
253
  tau_est[idx] = 0.; eta_est[:,idx] = 0.; var_est[idx] = np.inf
254
254
 
255
255
  return eta_est, tau_est, var_est
@@ -0,0 +1 @@
1
+ __version__ = "0.0.5"
@@ -196,7 +196,7 @@ def estimate_r(Y, X, A, r_max, c=1.,
196
196
  Results of the number of latent factors.
197
197
  '''
198
198
  a, d = A.shape[1], X.shape[1]
199
- X = np.hstack((X, A))
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)
@@ -41,15 +41,29 @@ def fit_glm(Y, X, A=None, family='gaussian', disp_family='poisson',
41
41
  A : array
42
42
  n x 1 vector of treatments or None
43
43
  family : str
44
- family of GLM to fit, can be one of: 'gaussian', 'poisson', 'nb'
44
+ Family of GLM to fit, can be one of: 'gaussian', 'poisson', 'nb'
45
45
  disp_glm : array or None
46
- dispersion parameter for negative binomial GLM
47
- return_df : bool
48
- whether to return results as DataFrame
49
- impute : bool
50
- whether to impute potential outcomes and get predicted values
46
+ Dispersion parameter for negative binomial GLM.
47
+ impute : bool or None
48
+ Whether to impute missing values in Y.
51
49
  offset : bool
52
- whether to use log of sum of Y as offset
50
+ Whether to use log of sum of Y as offset.
51
+ shrinkage : bool
52
+ Whether to use regularized GLM.
53
+ alpha : float
54
+ Regularization parameter for regularized GLM.
55
+ maxiter : int
56
+ Maximum number of iterations for GLM fitting.
57
+ thres_disp : float
58
+ Threshold for dispersion parameter for negative binomial GLM.
59
+ n_jobs : int
60
+ Number of jobs to run in parallel.
61
+ random_state : int
62
+ Random seed for reproducibility.
63
+ verbose : bool
64
+ Whether to print progress messages.
65
+ kwargs : dict
66
+ Additional arguments to pass to GLM fitting.
53
67
 
54
68
  Returns
55
69
  -------
@@ -261,7 +261,7 @@ def alter_min(
261
261
  kwargs_ls['alpha'] = kwargs_ls['alpha']
262
262
  if verbose:
263
263
  pprint.pprint({'kwargs_glm':kwargs_glm,'kwargs_ls':kwargs_ls,'kwargs_es':kwargs_es}, compact=True)
264
- pprint.pprint(f'Fitting GCATE (step {1 if P1 is None else 2})...')
264
+ pprint.pprint(f'Fitting GCATE (step {2 if P1 is None else 1})...')
265
265
  hist = [func_val_pre]
266
266
  es = Early_Stopping(**kwargs_es)
267
267
  with tqdm(np.arange(kwargs_es['max_iters']), disable=not verbose) as pbar:
@@ -49,6 +49,8 @@ def prep_causarray_data(Y, A, X=None, X_A=None, intercept=True):
49
49
  Y = np.minimum(Y, np.round(np.quantile(np.max(Y, 0), 0.999)))
50
50
  if not isinstance(A, pd.DataFrame):
51
51
  A = np.asarray(A)
52
+ if A.ndim == 1:
53
+ A = A[:, None]
52
54
 
53
55
  X = np.zeros((Y.shape[0], 0)) if X is None else np.asarray(X)
54
56
  X_A = X if X_A is None else np.asarray(X_A)
@@ -1 +0,0 @@
1
- __version__ = "0.0.3"
File without changes
File without changes
File without changes