ScreenPro2 0.5.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.
pyproject.toml ADDED
@@ -0,0 +1,30 @@
1
+ [tool.poetry]
2
+ name = "ScreenPro2"
3
+ version = "0.5.0"
4
+ description = "Flexible analysis of high-content CRISPR screening"
5
+ authors = [
6
+ "Abe Arab <abea@arcinstitute.org>"
7
+ ]
8
+ license = "MIT"
9
+ readme = "README.md"
10
+ homepage = "https://github.com/ArcInstitute/ScreenPro2"
11
+ repository = "https://github.com/ArcInstitute/ScreenPro2"
12
+ keywords = ["CRISPR", "screening", "bioinformatics"]
13
+ packages = [
14
+ { include = "screenpro" },
15
+ { include = "pyproject.toml" },
16
+ ]
17
+
18
+ [tool.poetry.dependencies]
19
+ python = ">=3.9"
20
+
21
+ [tool.poetry.scripts]
22
+ screenpro = "screenpro.main:main"
23
+
24
+ [tool.poetry.group.test.dependencies]
25
+ pytest = "*"
26
+ tomli = "*"
27
+
28
+ [build-system]
29
+ requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
30
+ build-backend = "poetry_dynamic_versioning.backend"
screenpro/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ ## Copyright (c) 2022-2025 ScreenPro2 Development Team.
2
+ ## All rights reserved.
3
+ ## Gilbart Lab, UCSF / Arc Institute.
4
+ ## Multi-Omics Tech Center, Arc Insititue.
5
+
6
+ '''ScreenPro2: A Python package for pooled CRISPR screens analysis
7
+
8
+ This package contains several modules, including:
9
+
10
+ **Main modules:**
11
+ - ngs: tools for generating counts from NGS data
12
+ - phenoscore: tools for calculating phenoscores
13
+ - assays: wrappers for analyzing CRISPR screens data from standard assays
14
+
15
+ **Additional modules:**
16
+ - load: tools for loading and saving data
17
+ - visualize: tools for visualizing data
18
+ - datasets: API for accessing pre-processed datasets
19
+ '''
20
+
21
+ from . import ngs
22
+ from . import load
23
+ from . import preprocessing as pp
24
+ from . import phenoscore as ps
25
+ from . import assays
26
+ from . import plotting as pl
27
+ from . import dashboard
28
+
29
+ from .ngs import GuideCounter
30
+ from .assays import PooledScreens, GImaps
31
+ from .dashboard import DrugScreenDashboard
screenpro/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ ## Copyright (c) 2022-2024 ScreenPro2 Development Team.
2
+ ## All rights reserved.
3
+ ## Gilbart Lab, UCSF / Arc Institute.
4
+ ## Multi-Omics Tech Center, Arc Insititue.
5
+
6
+ from .main import main
7
+
8
+ main()
@@ -0,0 +1,465 @@
1
+ ## Copyright (c) 2022-2025 ScreenPro2 Development Team.
2
+ ## All rights reserved.
3
+ ## Gilbart Lab, UCSF / Arc Institute.
4
+ ## Multi-Omics Tech Center, Arc Insititue.
5
+
6
+ """Assays module
7
+
8
+ """
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ import anndata as ad
13
+ import scanpy as sc
14
+ from typing import Literal
15
+
16
+ from ..phenoscore import (
17
+ runPhenoScore, getPhenotypeData,
18
+ runDESeq, extractDESeqResults,
19
+ )
20
+ from ..preprocessing import addPseudoCount, findLowCounts, normalizeSeqDepth
21
+ from ..phenoscore._annotate import annotateScoreTable, hit_dict
22
+ from ..plotting import volcano_plot, label_resistance_hit, label_sensitivity_hit
23
+
24
+ import warnings
25
+ from copy import copy
26
+
27
+
28
+ class PooledScreens(object):
29
+ """
30
+ pooledScreens class for processing CRISPR screen datasets
31
+ """
32
+
33
+ def __init__(self, adata, test='ttest', n_reps=3, verbose=False):
34
+ """
35
+ Args:
36
+ adata (AnnData): AnnData object with adata.X as a matrix of sgRNA counts
37
+ test (str): statistical test to use for calculating phenotype scores
38
+ n_reps (int): number of replicates to use for calculating phenotype scores
39
+ verbose (bool): whether to print verbose output
40
+ """
41
+ self.adata = adata.copy()
42
+ self.pdata = None
43
+ self.test = test
44
+ self.n_reps = n_reps
45
+ self.phenotypes = {}
46
+ self.verbose = verbose
47
+
48
+ def copy(self):
49
+ return copy(self)
50
+
51
+ def _add_phenotype_results(self, run_name, phenotype_name, phenotype_table):
52
+ if phenotype_name in self.phenotypes[run_name]['results'].keys():
53
+ raise ValueError(f"Phenotype '{phenotype_name}' already exists in self.phenotypes['results']!")
54
+ self.phenotypes[run_name]['results'][phenotype_name] = phenotype_table
55
+
56
+ def _calculateGrowthFactor(self, untreated, treated, db_rate_col):
57
+ """
58
+ Calculate growth factor for gamma, tau, or rho score per replicates.
59
+
60
+ Parameters:
61
+ untreated (str): untreated condition
62
+ treated (str): treated condition
63
+ db_rate_col (str): column name for doubling rate
64
+
65
+ Returns:
66
+ pd.DataFrame: growth factor dataframe
67
+ """
68
+ adat = self.adata.copy()
69
+ if 'condition' not in adat.obs.columns or 'replicate' not in adat.obs.columns:
70
+ raise ValueError("The 'condition' and 'replicate' columns self.adata.")
71
+ growth_factors = []
72
+ # calculate growth factor for gamma, tau, or rho score per replicates
73
+ for replicate in adat.obs.replicate.unique():
74
+ db_untreated = adat.obs.query(f'condition == "{untreated}" & replicate == {str(replicate)}')[db_rate_col][0]
75
+ db_treated = adat.obs.query(f'condition == "{treated}" & replicate == {str(replicate)}')[db_rate_col][0]
76
+
77
+ growth_factors.append(('gamma', db_untreated, replicate, f'gamma_replicate_{replicate}'))
78
+ growth_factors.append(('tau', db_treated, replicate, f'tau_replicate_{replicate}'))
79
+ growth_factors.append(('rho', np.abs(db_untreated - db_treated), replicate, f'rho_replicate_{replicate}'))
80
+
81
+ out = pd.DataFrame(growth_factors, columns=['score', 'growth_factor', 'replicate', 'index']).set_index('index')
82
+ out.index.name = None
83
+
84
+ return out
85
+
86
+ def _getTreatmentDoublingRate(self, untreated, treated, db_rate_col):
87
+ if 'pop_doubling' not in self.adata.obs.columns or db_rate_col == None:
88
+ warnings.warn('No doubling rate information provided.')
89
+ db_untreated = 1
90
+ db_treated = 1
91
+ db_diff = 1
92
+ growth_factor_table = None
93
+
94
+ else:
95
+ growth_factor_table = self._calculateGrowthFactor(
96
+ untreated = untreated, treated = treated, db_rate_col = db_rate_col
97
+ )
98
+
99
+ db_untreated=growth_factor_table.query(f'score=="gamma"')['growth_factor'].mean()
100
+ db_treated=growth_factor_table.query(f'score=="tau"')['growth_factor'].mean()
101
+ db_diff = np.abs(db_untreated - db_treated)
102
+
103
+ return db_untreated, db_treated, db_diff
104
+
105
+ def _auto_run_name(self):
106
+ if len(list(self.phenotypes.keys())) == 1:
107
+ run_name = list(self.phenotypes.keys())[0]
108
+ else:
109
+ raise ValueError(
110
+ 'Multiple phenotype calculation runs found.'
111
+ 'Please specify run_name. Available runs: '
112
+ '' + ', '.join(self.phenotypes.keys())
113
+ )
114
+ return run_name
115
+
116
+ def filterLowCounts(self, filter_type='all', minimum_reads=1):
117
+ """
118
+ Filter low counts in adata.X
119
+ """
120
+ findLowCounts(
121
+ self.adata,
122
+ filter_type=filter_type,
123
+ minimum_reads=minimum_reads,
124
+ verbose=self.verbose
125
+ )
126
+
127
+ self.adata = self.adata[:,~self.adata.var.low_count].copy()
128
+
129
+ def countNormalization(self, pseudo_count_value=0.5):
130
+ """
131
+ Preprocess and normalize the counts data in adata.X
132
+
133
+ Steps:
134
+ 1. Add pseudocount to counts
135
+ 2. Normalize counts by sequencing depth
136
+
137
+ """
138
+ self.adata.layers['raw_counts'] = self.adata.X.copy()
139
+
140
+ # add pseudocount
141
+ addPseudoCount(self.adata, behavior='default', value=pseudo_count_value)
142
+
143
+ if self.verbose: print('Pseudocount added to counts.')
144
+
145
+ # normalize counts by sequencing depth
146
+ normalizeSeqDepth(self.adata)
147
+
148
+ if self.verbose: print('Counts normalized by sequencing depth.')
149
+
150
+ def calculateDrugScreenDESeq(self, untreated, treated, t0=None, run_name='pyDESeq2', **kwargs):
151
+ """
152
+ Calculate DESeq2 results for a given drug screen dataset.
153
+
154
+ Args:
155
+ design (str): design matrix for DESeq2-based analysis
156
+ untreated (str): name of the untreated condition
157
+ treated (str): name of the treated condition
158
+ t0 (str): name of the untreated condition
159
+ run_name (str): name for the phenotype calculation run
160
+ **kwargs: additional arguments to pass to runDESeq
161
+ """
162
+ if run_name in self.phenotypes.keys():
163
+ raise ValueError(f"Phenotype calculation run '{run_name}' already exists in self.phenoypes!")
164
+ else:
165
+ self.phenotypes[run_name] = {}
166
+
167
+ self.phenotypes[run_name]['config'] = {
168
+ 'method':'pyDESeq2',
169
+ 'untreated':untreated,
170
+ 'treated':treated,
171
+ 't0':t0,
172
+ 'n_reps':self.n_reps,
173
+ }
174
+ self.phenotypes[run_name]['results'] = {}
175
+
176
+ if type(treated) != list: treated = [treated]
177
+
178
+ # run pyDESeq2 analysis
179
+ adt = self.adata.copy()
180
+ adt.X = adt.layers['raw_counts']
181
+ dds = runDESeq(adt, 'condition', **kwargs)
182
+
183
+ # extract comparison results
184
+ if t0 != None and type(t0) == str:
185
+ # Calculate `gamma`, `rho`, and `tau` phenotype scores
186
+ gamma_name, gamma = extractDESeqResults(
187
+ dds, design='condition', ref_level=t0, tested_level=untreated, **kwargs
188
+ )
189
+ self._add_phenotype_results(run_name, f'gamma:{gamma_name}', gamma)
190
+
191
+ for tr in treated:
192
+ tau_name, tau = extractDESeqResults(
193
+ dds, design='condition', ref_level=t0, tested_level=tr, **kwargs
194
+ )
195
+ self._add_phenotype_results(run_name, f'tau:{tau_name}', tau)
196
+
197
+ for tr in treated:
198
+ rho_name, rho = extractDESeqResults(
199
+ dds, design='condition', ref_level=untreated, tested_level=tr, **kwargs
200
+ )
201
+ self._add_phenotype_results(run_name, f'rho:{rho_name}', rho)
202
+
203
+ def calculateDrugScreen(self, score_level: Literal["compare_reps", "compare_guides"], untreated: str, treated: str, t0: str=None, db_rate_col: str='pop_doubling', run_name: str=None, count_filter_threshold: int=40, count_filter_type: Literal['mean', 'both', 'either']='mean', **kwargs):
204
+ """
205
+ Calculate `gamma`, `rho`, and `tau` phenotype scores for a drug screen dataset in a given `score_level`. This function
206
+ is a wrapper around runPhenoScore. Check the args of runPhenoScore carefully before using it.
207
+
208
+ For a given phenotype score, runPhenoScore implements a count filter threshold. By default this threshold changes
209
+ any guide or target whose mean count across replicates being compared is <40 to NAs. Because this can lead to
210
+ unexpected behavior when the user relies on filterLowCounts, we specify both the count_filter_threshold and
211
+ count_filter_type arguments of runPhenoScore explicitly here.
212
+
213
+ self.adata.obs must have a 'condition' column. If doubling infomation is provided, it also needs a 'replicate' column.
214
+
215
+ Args:
216
+ score_level (str): name of the score level. Must be "compare_reps" or "compare_guides"
217
+ untreated (str): name of the untreated condition
218
+ treated (str): name of the treated condition
219
+ t0 (str): name of the untreated condition
220
+ db_rate_col (str): column name for the doubling rate, default is 'pop_doubling'
221
+ run_name (str): name for the phenotype calculation run
222
+ count_filter_threshold (int): filter threshold for counts across compared replicates. Default is 40.
223
+ count_filter_type (str): type of filter for counts across replicates. Default is 'mean.'
224
+ **kwargs: additional arguments to pass to runPhenoScore
225
+ """
226
+ if not run_name: run_name = score_level
227
+ if run_name in self.phenotypes.keys():
228
+ raise ValueError(f"Phenotype calculation run '{run_name}' already exists in self.phenoypes!")
229
+ else:
230
+ self.phenotypes[run_name] = {}
231
+ self.phenotypes[run_name]['config'] = {
232
+ 'method':'ScreenPro2 - phenoscore',
233
+ 'untreated':untreated,
234
+ 'treated':treated,
235
+ 't0':t0,
236
+ 'n_reps':self.n_reps,
237
+ 'test':self.test,
238
+ 'score_level':score_level,
239
+ }
240
+ self.phenotypes[run_name]['results'] = {}
241
+
242
+ if type(treated) != list: treated = [treated]
243
+
244
+ # calculate phenotype scores: gamma, tau, rho
245
+ if t0 != None and type(t0) == str:
246
+ db_untreated,_,_ = self._getTreatmentDoublingRate(untreated, treated[0], db_rate_col)
247
+ gamma_name, gamma = runPhenoScore(
248
+ self.adata, cond_ref=t0, cond_test=untreated, growth_rate=db_untreated,
249
+ n_reps=self.n_reps,
250
+ test=self.test, score_level=score_level,
251
+ count_filter_threshold=count_filter_threshold,
252
+ count_filter_type=count_filter_type,
253
+ **kwargs
254
+ )
255
+ self._add_phenotype_results(run_name, f'gamma:{gamma_name}', gamma)
256
+
257
+ for tr in treated:
258
+ _, db_tr, db_diff = self._getTreatmentDoublingRate(untreated, tr, db_rate_col)
259
+ if t0 != None and type(t0) == str:
260
+ tau_name, tau = runPhenoScore(
261
+ self.adata, cond_ref=t0, cond_test=tr, growth_rate=db_tr,
262
+ n_reps=self.n_reps,
263
+ test=self.test, score_level=score_level,
264
+ count_filter_threshold=count_filter_threshold,
265
+ count_filter_type=count_filter_type,
266
+ **kwargs
267
+ )
268
+ self._add_phenotype_results(run_name, f'tau:{tau_name}', tau)
269
+
270
+ #TODO: warning / error if db_untreated and db_treated are too close, i.e. growth_rate ~= 0.
271
+ rho_name, rho = runPhenoScore(
272
+ self.adata, cond_ref=untreated, cond_test=tr, growth_rate=db_diff,
273
+ n_reps=self.n_reps,
274
+ test=self.test, score_level=score_level,
275
+ count_filter_threshold=count_filter_threshold,
276
+ count_filter_type=count_filter_type,
277
+ **kwargs
278
+ )
279
+ self._add_phenotype_results(run_name, f'rho:{rho_name}', rho)
280
+
281
+ def calculateFlowBasedScreen(self, low_bin, high_bin, score_level, run_name=None, **kwargs):
282
+ """
283
+ Calculate phenotype scores for a flow-based screen dataset.
284
+
285
+ Args:
286
+ low_bin (str): name of the low bin condition
287
+ high_bin (str): name of the high bin condition
288
+ score_level (str): name of the score level
289
+ run_name (str): name for the phenotype calculation run
290
+ **kwargs: additional arguments to pass to runPhenoScore
291
+ """
292
+ if not run_name: run_name = score_level
293
+ if run_name in self.phenotypes.keys():
294
+ raise ValueError(f"Phenotype calculation run '{run_name}' already exists in self.phenoypes!")
295
+ else:
296
+ self.phenotypes[run_name] = {}
297
+ self.phenotypes[run_name]['config'] = {
298
+ 'method':'ScreenPro2 - phenoscore',
299
+ 'low_bin':low_bin,
300
+ 'high_bin':high_bin,
301
+ 'test':self.test,
302
+ 'score_level':score_level,
303
+ }
304
+ self.phenotypes[run_name]['results'] = {}
305
+
306
+ # calculate phenotype scores
307
+ delta_name, delta = runPhenoScore(
308
+ self.adata, cond_ref=low_bin, cond_test=high_bin, n_reps=self.n_reps,
309
+ test=self.test, score_level=score_level,
310
+ **kwargs
311
+ )
312
+
313
+ self._add_phenotype_results(run_name, f'delta:{delta_name}', delta)
314
+
315
+ def listPhenotypeScores(self, run_name='auto'):
316
+ """
317
+ List available phenotype scores for a given run_name
318
+
319
+ Args:
320
+ run_name (str): name of the phenotype calculation run to retrieve
321
+ """
322
+ if run_name == 'auto': run_name = self._auto_run_name()
323
+
324
+ out = list(self.phenotypes[run_name]['results'].keys())
325
+
326
+ return out
327
+
328
+ def getPhenotypeScores(self, phenotype_name, threshold, run_name='auto', **kwargs):
329
+ """
330
+ Get phenotype scores for a given phenotype_name
331
+
332
+ Args:
333
+ phenotype_name (str): name of the phenotype score
334
+ run_name (str): name of the phenotype calculation run to retrieve
335
+ """
336
+ if run_name == 'auto': run_name = self._auto_run_name()
337
+
338
+ score_tag, _ = phenotype_name.split(':')
339
+
340
+ out = annotateScoreTable(
341
+ self.phenotypes[run_name]['results'][phenotype_name],
342
+ up_hit=hit_dict[score_tag]['up_hit'],
343
+ down_hit=hit_dict[score_tag]['down_hit'],
344
+ threshold=threshold,
345
+ **kwargs
346
+ )
347
+
348
+ return out
349
+
350
+ def buildPhenotypeData(self, run_name='auto',db_rate_col='pop_doubling', **kwargs):
351
+ if run_name == 'auto': run_name = self._auto_run_name()
352
+ if run_name=='compare_reps':
353
+ pass
354
+ else:
355
+ raise ValueError('Only `compare_reps` run_name is supported for now!')
356
+
357
+ untreated = self.phenotypes[run_name]['config']['untreated']
358
+
359
+ pdata_list = []
360
+
361
+ for phenotype_name in self.listPhenotypeScores(run_name=run_name):
362
+
363
+ score_tag, comparison = phenotype_name.split(':')
364
+ cond_test, cond_ref = comparison.split('_vs_')
365
+
366
+ #TODO: fix `_calculateGrowthFactor` and `_getTreatmentDoublingRate`
367
+ if db_rate_col:
368
+ growth_rate_reps = self._calculateGrowthFactor(
369
+ untreated = untreated,
370
+ treated = cond_test, # should be part of "treated" list!
371
+ db_rate_col = db_rate_col
372
+ ).query(
373
+ f'score=="{score_tag}"'
374
+ ).set_index('replicate')['growth_factor'].to_dict()
375
+
376
+ else:
377
+ growth_rate_reps=None
378
+
379
+ pdata = getPhenotypeData(
380
+ self.adata, score_tag=score_tag,
381
+ cond_ref=cond_ref, cond_test=cond_test,
382
+ growth_rate_reps=growth_rate_reps,
383
+ **kwargs
384
+ )
385
+
386
+ pdata_list.append(pdata)
387
+
388
+ self.pdata = ad.concat(pdata_list, axis=0)
389
+ self.pdata.var = self.adata.var.copy()
390
+
391
+ def drawVolcano(
392
+ self, ax,
393
+ phenotype_name,
394
+ threshold,
395
+ dot_size=1,
396
+ run_name='auto',
397
+ score_col='score',
398
+ pvalue_col='pvalue',
399
+ xlabel='auto',
400
+ ylabel='-log10(pvalue)',
401
+ xlims='auto',
402
+ ylims='auto',
403
+ ctrl_label='negative_control',
404
+ resistance_hits=None,
405
+ sensitivity_hits=None,
406
+ size_txt=None,
407
+ t_x=0, t_y=0,
408
+ **args
409
+ ):
410
+ if run_name == 'auto': run_name = self._auto_run_name()
411
+
412
+ score_tag, _ = phenotype_name.split(':')
413
+
414
+ df = self.phenotypes[run_name]['results'][phenotype_name].dropna()
415
+
416
+ df = annotateScoreTable(
417
+ df,
418
+ up_hit=hit_dict[score_tag]['up_hit'],
419
+ down_hit=hit_dict[score_tag]['down_hit'],
420
+ score_col=score_col, pvalue_col=pvalue_col,
421
+ ctrl_label=ctrl_label,
422
+ threshold=threshold,
423
+ )
424
+
425
+ df['-log10(pvalue)'] = -np.log10(df[pvalue_col])
426
+
427
+ if xlabel == 'auto':
428
+ xlabel = phenotype_name.replace(':', ': ').replace('_', ' ')
429
+
430
+ volcano_plot(ax, df,
431
+ up_hit=hit_dict[score_tag]['up_hit'],
432
+ down_hit=hit_dict[score_tag]['down_hit'],
433
+ score_col=score_col, pvalue_col=pvalue_col,
434
+ xlabel=xlabel, ylabel=ylabel,
435
+ dot_size=dot_size, xlims=xlims, ylims=ylims,
436
+ ctrl_label=ctrl_label,
437
+ **args)
438
+
439
+ if resistance_hits != None:
440
+ if type(resistance_hits) != list: resistance_hits = [resistance_hits]
441
+ for hit in resistance_hits:
442
+ label_resistance_hit(
443
+ ax=ax, df_in=df, label=hit,
444
+ x_col=score_col,
445
+ y_col='-log10(pvalue)',
446
+ size=dot_size * 2,
447
+ size_txt=size_txt,
448
+ t_x=t_x, t_y=t_y
449
+ )
450
+
451
+ if sensitivity_hits != None:
452
+ if type(sensitivity_hits) != list: sensitivity_hits = [sensitivity_hits]
453
+ for hit in sensitivity_hits:
454
+ label_sensitivity_hit(
455
+ ax=ax, df_in=df, label=hit,
456
+ x_col=score_col,
457
+ y_col='-log10(pvalue)',
458
+ size=dot_size * 2,
459
+ size_txt=size_txt,
460
+ t_x=t_x, t_y=t_y
461
+ )
462
+
463
+
464
+ class GImaps(object):
465
+ pass