causarray 0.0.4__tar.gz → 0.0.6__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,3 +1,6 @@
1
+ # Planning documents
2
+ plan/
3
+
1
4
  # Byte-compiled / optimized / DLL files
2
5
  __pycache__/
3
6
  *.py[cod]
@@ -160,5 +163,20 @@ cython_debug/
160
163
  #.idea/
161
164
 
162
165
 
163
- test*
166
+ # Generic module tests under tests/ ARE tracked (regression coverage for
167
+ # the causarray API). Exclude only benchmark / scratch files that are
168
+ # not part of the unit-test suite — add new exclusions here when
169
+ # benchmark or tutorial-coupled test files appear.
170
+ tests/benchmark_adamson.py
171
+
164
172
  causarray/___*.py
173
+ /docs/source/tutorial/adamson
174
+
175
+ # Replogle tutorial: large data files (regenerate with prep_tutorial_data.py or
176
+ # download from the project data repository; replogle_subset.h5ad is ~2 GB)
177
+ docs/source/tutorial/replogle/replogle_subset.h5ad
178
+ docs/source/tutorial/replogle/replogle_normed.h5ad
179
+ docs/source/tutorial/replogle/replogle_results.h5
180
+
181
+ # Unrelated tutorial directory (not part of causarray package docs)
182
+ docs/source/tutorial/scp/
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: causarray
3
- Version: 0.0.4
3
+ Version: 0.0.6
4
4
  Summary: causarray is a Python module for simultaneous causal inference with an array of outcomes.
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
- Maintainer-email: Jin-Hong Du <jinhongd@andrew.cmu.com>
5
+ Author-email: Jin-Hong Du <jinhongd@hku.hk>, Maya Shen <myshen@andrew.cmu.edu>, Hansruedi Mathys <mathysh@pitt.edu>, Kathryn Roeder <jinhongd@hku.hk>
6
+ Maintainer-email: Jin-Hong Du <jinhongd@hku.hk>
7
7
  License: MIT License
8
8
  License-File: LICENSE
9
9
  Classifier: Intended Audience :: Developers
@@ -49,17 +49,44 @@ The module can be installed via PyPI:
49
49
  pip install causarray
50
50
  ```
51
51
 
52
+ For optimal parallel performance, we recommend installing `llvm-openmp` if using conda:
53
+ ```cmd
54
+ conda install -c conda-forge llvm-openmp
55
+ ```
56
+
52
57
  For `R` users, `reticulate` can be used to call `causarray` from `R`.
53
58
  The documentation and tutorials using both `Python` and `R` are available at [causarray.readthedocs.io](https://causarray.readthedocs.io/en/latest/).
54
59
 
55
60
 
56
61
 
57
- ## Logs
62
+ ## Batch fitting for large-scale screens
63
+
64
+ For screens with hundreds to thousands of perturbations, use the batch API
65
+ so that peak memory is bounded by one batch at a time:
66
+
67
+ ```python
68
+ from causarray import gcate_lfc_batch
69
+
70
+ df_res = gcate_lfc_batch(
71
+ Y, X, A, r,
72
+ batch_size=10, # perturbations per batch (or use n_batches= for a fixed count)
73
+ max_cells=2000, # max pert cells per batch (ctrl added on top)
74
+ n_ctrl=2000, # fixed ctrl subsample shared across batches
75
+ cache_path='results.h5', # resume if interrupted
76
+ verbose=True,
77
+ )
78
+ ```
79
+
80
+ See the [Replogle-E-K562 tutorial](https://causarray.readthedocs.io/en/latest/)
81
+ for a demonstration on 200 perturbations from a genome-wide CRISPRi screen.
82
+
83
+ ## Changelog
58
84
 
59
85
  - [x] (2025-01-30) Python package released on PyPI
60
- - [x] (2025-02-01) code for reproducing figures in paper
86
+ - [x] (2025-02-01) Code for reproducing figures in paper
61
87
  - [x] (2025-02-02) Tutorial for Python and R
62
- - [ ] Documentation
88
+ - [x] (2026-05-31) Batch fitting API (`gcate_lfc_batch`) for large-scale screens
89
+ - [x] (2026-05-31) Documentation at [causarray.readthedocs.io](https://causarray.readthedocs.io/en/latest/)
63
90
 
64
91
 
65
92
  <!--
@@ -24,17 +24,44 @@ The module can be installed via PyPI:
24
24
  pip install causarray
25
25
  ```
26
26
 
27
+ For optimal parallel performance, we recommend installing `llvm-openmp` if using conda:
28
+ ```cmd
29
+ conda install -c conda-forge llvm-openmp
30
+ ```
31
+
27
32
  For `R` users, `reticulate` can be used to call `causarray` from `R`.
28
33
  The documentation and tutorials using both `Python` and `R` are available at [causarray.readthedocs.io](https://causarray.readthedocs.io/en/latest/).
29
34
 
30
35
 
31
36
 
32
- ## Logs
37
+ ## Batch fitting for large-scale screens
38
+
39
+ For screens with hundreds to thousands of perturbations, use the batch API
40
+ so that peak memory is bounded by one batch at a time:
41
+
42
+ ```python
43
+ from causarray import gcate_lfc_batch
44
+
45
+ df_res = gcate_lfc_batch(
46
+ Y, X, A, r,
47
+ batch_size=10, # perturbations per batch (or use n_batches= for a fixed count)
48
+ max_cells=2000, # max pert cells per batch (ctrl added on top)
49
+ n_ctrl=2000, # fixed ctrl subsample shared across batches
50
+ cache_path='results.h5', # resume if interrupted
51
+ verbose=True,
52
+ )
53
+ ```
54
+
55
+ See the [Replogle-E-K562 tutorial](https://causarray.readthedocs.io/en/latest/)
56
+ for a demonstration on 200 perturbations from a genome-wide CRISPRi screen.
57
+
58
+ ## Changelog
33
59
 
34
60
  - [x] (2025-01-30) Python package released on PyPI
35
- - [x] (2025-02-01) code for reproducing figures in paper
61
+ - [x] (2025-02-01) Code for reproducing figures in paper
36
62
  - [x] (2025-02-02) Tutorial for Python and R
37
- - [ ] Documentation
63
+ - [x] (2026-05-31) Batch fitting API (`gcate_lfc_batch`) for large-scale screens
64
+ - [x] (2026-05-31) Documentation at [causarray.readthedocs.io](https://causarray.readthedocs.io/en/latest/)
38
65
 
39
66
 
40
67
  <!--
@@ -1,9 +1,13 @@
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
6
+ import causarray.gcate_glm as _gcate_glm # module-qualified so _USE_FAST_BACKEND changes take effect at call time
5
7
  from causarray.utils import *
6
8
  from causarray.utils import _filter_params
9
+ from joblib import Parallel, delayed
10
+ from tqdm import tqdm
7
11
  import pprint
8
12
 
9
13
  from sklearn.model_selection import KFold, ShuffleSplit
@@ -82,10 +86,15 @@ def cross_fitting(
82
86
  pprint.pprint(params_ps)
83
87
  pprint.pprint(params_glm)
84
88
 
85
- if K>1:
86
- # Initialize KFold cross-validator
87
- kf = KFold(n_splits=K, random_state=0, shuffle=True)
88
- folds = kf.split(X)
89
+ if K > 1:
90
+ n_samples = X.shape[0]
91
+ if K >= n_samples:
92
+ # Use Leave-One-Out Cross-Validation
93
+ folds = [([i for i in range(n_samples) if i != j], [j]) for j in range(n_samples)]
94
+ else:
95
+ # Initialize KFold cross-validator
96
+ kf = KFold(n_splits=int(K), random_state=0, shuffle=True)
97
+ folds = kf.split(X)
89
98
  else:
90
99
  folds = [(np.arange(X.shape[0]), np.arange(X.shape[0]))]
91
100
 
@@ -93,7 +102,29 @@ def cross_fitting(
93
102
  fit_pi = True if pi_hat is None else False
94
103
  pi_hat = np.zeros_like(A, dtype=float) if fit_pi else pi_hat
95
104
  fit_Y = True if Y_hat is None else False
96
- Y_hat = np.zeros((Y.shape[0],Y.shape[1],A.shape[1],2), dtype=float) if fit_Y else Y_hat
105
+ if fit_Y:
106
+ _yhat_gb = Y.shape[0] * Y.shape[1] * A.shape[1] * 2 * 8 / 1e9
107
+ _mem_limit_gb = kwargs.get('mem_limit_gb', None)
108
+ if _mem_limit_gb is not None and _yhat_gb > _mem_limit_gb:
109
+ import warnings
110
+ warnings.warn(
111
+ f"Y_hat allocation ({_yhat_gb:.1f} GB as float64) exceeds "
112
+ f"mem_limit_gb={_mem_limit_gb} GB; using float32 to halve peak memory.",
113
+ ResourceWarning, stacklevel=3,
114
+ )
115
+ Y_hat = np.zeros((Y.shape[0], Y.shape[1], A.shape[1], 2), dtype=np.float32)
116
+ else:
117
+ Y_hat = np.zeros((Y.shape[0], Y.shape[1], A.shape[1], 2), dtype=float)
118
+
119
+ # perform ECV at once
120
+ if fit_pi and ps_model == 'random_forest_cv':
121
+ info_ecv = run_ecv(X_A, A, **params_ps)
122
+ func_ps, params_ps = _get_func_ps(ps_model, verbose=False, ecv=False,
123
+ kwargs_ensemble=info_ecv['best_params_ensemble'], kwargs_regr=info_ecv['best_params_regr'])
124
+ pprint.pprint('Best parameters for the regression model:')
125
+ pprint.pprint(info_ecv['best_params_regr'])
126
+ pprint.pprint('Best parameters for the ensemble model:')
127
+ pprint.pprint(info_ecv['best_params_ensemble'])
97
128
 
98
129
  # Perform cross-fitting
99
130
  for train_index, test_index in folds:
@@ -127,9 +158,19 @@ def cross_fitting(
127
158
 
128
159
  if fit_Y:
129
160
  if verbose: pprint.pprint('Fit outcome models...')
161
+ # Subset offset to training fold (for fitting) and test fold (for
162
+ # imputation) when it is a pre-computed array, so that
163
+ # ``fit_glm_auto`` receives arrays with matching leading
164
+ # dimensions in both stages.
165
+ params_glm_fold = params_glm
166
+ offset_test_arr = None
167
+ if 'offset' in params_glm and isinstance(params_glm['offset'], np.ndarray):
168
+ params_glm_fold = dict(params_glm)
169
+ params_glm_fold['offset'] = params_glm['offset'][train_index]
170
+ offset_test_arr = params_glm['offset'][test_index]
130
171
  # Fit GLM on training data and predict on test data
131
- res = fit_glm(Y_train, X_train, A_train, family=family, alpha=glm_alpha,
132
- impute=X_test, **params_glm)
172
+ res = _gcate_glm.fit_glm_auto(Y_train, X_train, A_train, family=family, alpha=glm_alpha,
173
+ impute=X_test, offset_test=offset_test_arr, **params_glm_fold)
133
174
  Y_hat[test_index,:,:,0] = res[1][0]
134
175
  Y_hat[test_index,:,:,1] = res[1][1]
135
176
 
@@ -178,8 +219,6 @@ def AIPW_mean(Y, A, mu, pi, positive=False):
178
219
  tau = np.mean(pseudo_y, axis=0)
179
220
 
180
221
  return tau, pseudo_y
181
-
182
-
183
222
 
184
223
 
185
224
 
@@ -188,51 +227,89 @@ def AIPW_mean(Y, A, mu, pi, positive=False):
188
227
 
189
228
 
190
229
 
191
- from joblib import Parallel, delayed
192
- from tqdm import tqdm
193
- from sklearn_ensemble_cv import reset_random_seeds, Ensemble, ECV
194
- from sklearn.tree import DecisionTreeRegressor
195
-
196
- def fit_rf(X, y, X_test=None, sample_weight=None, M=100, M_max=1000,
230
+ def run_ecv(
231
+ X, y, M=200, M_max=1000,
197
232
  # fixed parameters for bagging regressor
198
- kwargs_ensemble={'verbose':1},
233
+ kwargs_ensemble={},
199
234
  # fixed parameters for decision tree
200
- kwargs_regr={'min_samples_leaf': 3}, # 'min_samples_split': 10, 'max_features':'sqrt'
235
+ kwargs_regr={},
201
236
  # grid search parameters
202
- grid_regr = {'max_depth': [11]},
203
- grid_ensemble = {'random_state': 0}, #'max_samples':np.linspace(0.25, 1., 4)
204
- ):
237
+ grid_regr={},
238
+ grid_ensemble={}
239
+ ):
240
+ """
241
+ Runs Ensemble Cross-Validation (ECV) to find the best hyperparameters.
242
+ """
243
+ kwargs_ensemble = {**{'verbose': 1, 'bootstrap': True}, **kwargs_ensemble}
244
+ kwargs_regr = {**{'min_samples_split': 20, 'min_samples_leaf': 10, 'max_features': 'sqrt', 'ccp_alpha': 0.02, 'class_weight': 'balanced'}, **kwargs_regr}
245
+ grid_regr = {**{'max_depth': [3, 5, 7]}, **grid_regr}
246
+ grid_ensemble = {**{'random_state': 0, 'max_samples': [0.4, 0.6, 0.8, 1.]}, **grid_ensemble}
205
247
 
206
248
  # Validate integer parameters
207
249
  M = int(M)
208
250
  M_max = int(M_max)
209
- # for kwargs in [kwargs_regr, kwargs_ensemble, grid_regr, grid_ensemble]:
210
- # for param in kwargs:
211
- # if param in ['max_depth', 'random_state', 'max_leaf_nodes'] and isinstance(kwargs[param], float):
212
- # kwargs[param] = int(kwargs[param])
213
251
 
214
252
  # Make sure y is 2D
215
253
  y = y.reshape(-1, 1) if y.ndim == 1 else y
216
254
 
217
255
  # Run ECV
218
- res_ecv, info_ecv = ECV(
219
- X, y, DecisionTreeRegressor, grid_regr, grid_ensemble,
220
- kwargs_regr, kwargs_ensemble,
256
+ _, info_ecv = ECV(
257
+ X, y, DecisionTreeClassifier, grid_regr, grid_ensemble,
258
+ kwargs_regr, kwargs_ensemble,
221
259
  M=M, M0=M, M_max=M_max, return_df=True
222
260
  )
223
261
 
224
262
  # Replace the in-sample best parameter for 'n_estimators' with extrapolated best parameter
225
263
  info_ecv['best_params_ensemble']['n_estimators'] = info_ecv['best_n_estimators_extrapolate']
226
264
 
265
+ return info_ecv
266
+
267
+
268
+ def fit_rf(
269
+ X, y, X_test=None, M=100, M_max=1000, ecv=True,
270
+ # fixed parameters for bagging regressor
271
+ kwargs_ensemble={},
272
+ # fixed parameters for decision tree
273
+ kwargs_regr={},
274
+ # grid search parameters
275
+ grid_regr={},
276
+ grid_ensemble={}
277
+ ):
278
+ """
279
+ Fits a Random Forest model using parameters found by ECV.
280
+ """
281
+
282
+ kwargs_ensemble = {**{'verbose': 1, 'bootstrap': True}, **kwargs_ensemble}
283
+ kwargs_regr = {**{'min_samples_split': 20, 'min_samples_leaf': 10, 'max_features': 'sqrt', 'ccp_alpha': 0.02, 'class_weight': 'balanced'}, **kwargs_regr}
284
+ grid_regr = {**{'max_depth': [3, 5, 7]}, **grid_regr}
285
+ grid_ensemble = {**{'random_state': 0, 'max_samples': [0.4, 0.6, 0.8, 1.]}, **grid_ensemble}
286
+
287
+ # Make sure y is 2D
288
+ y_2d = y.reshape(-1, 1) if y.ndim == 1 else y
289
+
290
+ if ecv:
291
+ # Get best parameters from ECV
292
+ info_ecv = run_ecv(
293
+ X, y_2d, M=M, M_max=M_max,
294
+ kwargs_ensemble=kwargs_ensemble,
295
+ kwargs_regr=kwargs_regr,
296
+ grid_regr=grid_regr,
297
+ grid_ensemble=grid_ensemble
298
+ )
299
+ params_regr = info_ecv['best_params_regr']
300
+ params_ensemble = info_ecv['best_params_ensemble']
301
+ else:
302
+ params_regr = kwargs_regr
303
+ params_ensemble = kwargs_ensemble
304
+
227
305
  # Fit the ensemble with the best CV parameters
228
306
  regr = Ensemble(
229
- estimator=DecisionTreeRegressor(**info_ecv['best_params_regr']),
230
- **info_ecv['best_params_ensemble']).fit(X, y, sample_weight=sample_weight)
231
-
307
+ estimator=DecisionTreeClassifier(**params_regr), **params_ensemble).fit(X, y_2d)
308
+
232
309
  # Predict
233
310
  if X_test is None:
234
311
  X_test = X
235
- return regr.predict(X_test).reshape(-1, y.shape[1])
312
+ return regr.predict(X_test).reshape(-1, y_2d.shape[1])
236
313
 
237
314
 
238
315
 
@@ -252,11 +329,7 @@ def fit_rf_ind_ps(X, Y, *args, **kwargs):
252
329
  def _fit(X, y, i_ctrl, *args, **kwargs):
253
330
  i_case = (y == 1.)
254
331
  i_cells = i_ctrl | i_case
255
- sample_weight = np.ones(y.shape[0])
256
- class_weight = len(y) / (2 * np.bincount(y.astype(int)))
257
- for a in range(2):
258
- sample_weight[y == a] = class_weight[a]
259
- return fit_rf(X[i_cells], y[i_cells], sample_weight=sample_weight[i_cells], *args, **kwargs)
332
+ return fit_rf(X[i_cells], y[i_cells], *args, **kwargs)
260
333
 
261
334
  Y_hat = Parallel(n_jobs=-1)(delayed(_fit)(X, Y[:,j], i_ctrl, *args, **kwargs)
262
335
  for j in tqdm(range(Y.shape[1])))
@@ -150,7 +150,7 @@ def fdx_control(
150
150
  return V
151
151
 
152
152
 
153
- def bh_correction(tvalues_init):
153
+ def bh_correction(tvalues_init, df=None):
154
154
  '''
155
155
  Perform BH correction.
156
156
 
@@ -179,7 +179,10 @@ def bh_correction(tvalues_init):
179
179
  qvals_adj = np.full(tvalues_init.shape, np.nan)
180
180
 
181
181
  if np.sum(idx) > 0:
182
- pvals[idx] = sp.stats.norm.sf(np.abs(tvalues_init[idx]))*2
182
+ if df is not None:
183
+ pvals[idx] = sp.stats.t.sf(np.abs(tvalues_init[idx]), df=df[idx]) * 2
184
+ else:
185
+ pvals[idx] = sp.stats.norm.sf(np.abs(tvalues_init[idx])) * 2
183
186
  qvals[idx] = multipletests(pvals[idx], alpha=0.05, method='fdr_bh')[1]
184
187
 
185
188
  # BH correction with empirical null adjustment
@@ -189,7 +192,10 @@ def bh_correction(tvalues_init):
189
192
  if mad>0:
190
193
  tvalues_init_adj = (tvalues_init - med) / mad
191
194
 
192
- pvals_adj[idx] = sp.stats.norm.sf(np.abs(tvalues_init_adj[idx]))*2
195
+ if df is not None:
196
+ pvals_adj[idx] = sp.stats.t.sf(np.abs(tvalues_init_adj[idx]), df=df[idx]) * 2
197
+ else:
198
+ pvals_adj[idx] = sp.stats.norm.sf(np.abs(tvalues_init_adj[idx])) * 2
193
199
  qvals_adj[idx] = multipletests(pvals_adj[idx], alpha=0.05, method='fdr_bh')[1]
194
200
 
195
201
  return pvals, qvals, pvals_adj, qvals_adj