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.
@@ -0,0 +1,293 @@
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
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import bokeh
10
+ import bokeh.plotting
11
+
12
+
13
+ class DataDashboard:
14
+
15
+ def __init__(self):
16
+ pass
17
+
18
+ def _new_plot(self,title,tooltips,width,height,toolbar_location):
19
+
20
+ TOOLS = "box_select,box_zoom,lasso_select,reset,save,wheel_zoom,pan,copy,undo,redo,reset,examine,fullscreen"
21
+
22
+ # create a new plot with a specific size
23
+ p = bokeh.plotting.figure(
24
+ sizing_mode="stretch_width",
25
+ tools=TOOLS,
26
+ tooltips=tooltips,
27
+ toolbar_location=toolbar_location,
28
+ title=title,
29
+ max_width=width, height=height,
30
+ )
31
+ p.toolbar.autohide = True
32
+ return p
33
+
34
+ def _get_html(self, p):
35
+ html = bokeh.embed.file_html(p, bokeh.resources.CDN, "")
36
+ return html
37
+
38
+
39
+ class DrugScreenDashboard(DataDashboard):
40
+
41
+ def __init__(
42
+ self, screen, treated, untreated,
43
+ t0='T0', threshold=3, ctrl_label='negative_control',
44
+ run_name='auto',
45
+ score_col='score', pvalue_col='pvalue'
46
+ ):
47
+ self.screen = screen
48
+ self.threshold = threshold
49
+ self.ctrl_label = ctrl_label
50
+ self.run_name = run_name
51
+ self.gamma_score_name = f'gamma:{untreated}_vs_{t0}'
52
+ self.rho_score_name = f'rho:{treated}_vs_{untreated}'
53
+ self.df = self._prep_data(screen, score_col=score_col, pvalue_col=pvalue_col)
54
+ self.plots = {}
55
+ super().__init__()
56
+
57
+ def _prep_data(self,screen, score_col='score', pvalue_col='pvalue'):
58
+
59
+ gamma = screen.getPhenotypeScores(
60
+ phenotype_name=self.gamma_score_name,
61
+ run_name=self.run_name,
62
+ threshold=self.threshold,
63
+ ctrl_label=self.ctrl_label,
64
+ score_col=score_col,
65
+ pvalue_col=pvalue_col
66
+ )
67
+
68
+ rho = screen.getPhenotypeScores(
69
+ phenotype_name=self.rho_score_name,
70
+ run_name=self.run_name,
71
+ threshold=self.threshold,
72
+ ctrl_label=self.ctrl_label,
73
+ score_col=score_col,
74
+ pvalue_col=pvalue_col
75
+ )
76
+
77
+ df = pd.DataFrame({
78
+ 'target': rho['target'],
79
+ 'rho_score': rho['score'],
80
+ 'rho_pvalue': rho[pvalue_col],
81
+ 'rho_label': rho['label'],
82
+ '-log10(rho_pvalue)': np.log10(rho[pvalue_col]) * -1,
83
+ 'gamma_score': gamma.loc[rho.index,'score'],
84
+ 'gamma_pvalue': gamma.loc[rho.index,pvalue_col],
85
+ 'gamma_label': gamma.loc[rho.index,'label'],
86
+ '-log10(gamma_pvalue)': np.log10(gamma.loc[rho.index,pvalue_col]) * -1,
87
+ })
88
+
89
+ return df
90
+
91
+ def _plot_scatter(
92
+ self,
93
+ x_source,y_source,
94
+ xaxis_label,yaxis_label,
95
+ up_hit, down_hit,
96
+ hit_label_col,
97
+ x_min, x_max, y_min, y_max,
98
+ title='',
99
+ dot_size=1,
100
+ width=500, height=400,
101
+ toolbar_location='below',
102
+ legend_loc="top_left",
103
+ ):
104
+ df = self.df.copy()
105
+ if y_max == 'auto': y_max = df[y_source].max() * 1.2
106
+ if x_max == 'auto': x_max = df[x_source].max() * 1.2
107
+ if y_min == 'auto': y_min = df[y_source].min() * 1.2
108
+ if x_min == 'auto': x_min = df[x_source].min() * 1.2
109
+
110
+ TOOLTIPS = [
111
+ ("name", "@target"),
112
+ ("rho score", "@rho_score"),
113
+ ("rho p-value", "@rho_pvalue"),
114
+ ("rho label", "@rho_label"),
115
+ ("gamma score", "@gamma_score"),
116
+ ("gamma p-value", "@gamma_pvalue"),
117
+ ("gamma label", "@gamma_label"),
118
+ ]
119
+
120
+ p = self._new_plot(
121
+ title=title,
122
+ tooltips=TOOLTIPS,
123
+ width=width,
124
+ height=height,
125
+ toolbar_location=toolbar_location
126
+ )
127
+
128
+ source = bokeh.models.ColumnDataSource(
129
+ df.loc[df[hit_label_col] == 'target_non_hit',:]
130
+ )
131
+ p.scatter(
132
+ x=x_source, y=y_source,
133
+ source=source,
134
+ alpha=0.2,
135
+ size=dot_size * 1.2,
136
+ color='gray',
137
+ legend_label='target_non_hit',
138
+ name='circles'
139
+ )
140
+
141
+ # size_mapper=bokeh.models.LinearInterpolator(
142
+ # x=[df['1/gamma_score'].min(),df['1/gamma_score'].max()],
143
+ # y=[1,100]
144
+ # )
145
+
146
+ source = bokeh.models.ColumnDataSource(
147
+ df.loc[df[hit_label_col] == up_hit,:]
148
+ )
149
+ p.scatter(
150
+ x=x_source, y=y_source,
151
+ source=source,
152
+ alpha=0.8,
153
+ size=dot_size * 1.2,
154
+ # size={'field':'1/gamma_score','transform':size_mapper},
155
+ color='#fcae91',
156
+ legend_label=up_hit,
157
+ name='circles'
158
+ )
159
+
160
+ source = bokeh.models.ColumnDataSource(
161
+ df.loc[df[hit_label_col] == down_hit,:]
162
+ )
163
+ p.scatter(
164
+ x=x_source, y=y_source,
165
+ source=source,
166
+ alpha=0.8,
167
+ # size={'field':'1/gamma_score','transform':size_mapper},
168
+ size=dot_size * 1.2,
169
+ color='#bdd7e7',
170
+ legend_label=down_hit,
171
+ name='circles'
172
+ )
173
+
174
+ source = bokeh.models.ColumnDataSource(
175
+ df.loc[df[hit_label_col] == self.ctrl_label,:]
176
+ )
177
+ p.scatter(
178
+ x=x_source, y=y_source,
179
+ source=source,
180
+ alpha=0.2,
181
+ size=dot_size*0.8,
182
+ color='silver',
183
+ legend_label=self.ctrl_label,
184
+ name='circles'
185
+ )
186
+
187
+ # Set x-axis and y-axis labels
188
+ p.xaxis.axis_label = xaxis_label
189
+ p.xaxis.axis_label_text_font_style = 'normal'
190
+ p.yaxis.axis_label = yaxis_label
191
+ p.yaxis.axis_label_text_font_style = 'normal'
192
+
193
+ # Set x-axis limits
194
+ p.x_range.start = x_min
195
+ p.x_range.end = x_max
196
+
197
+ # Set y-axis limits
198
+ p.y_range.start = y_min
199
+ p.y_range.end = y_max
200
+
201
+ # Add legend
202
+ if legend_loc == False or legend_loc == None:
203
+ p.legend.visible = False
204
+ else:
205
+ p.legend.location = legend_loc
206
+
207
+ p.title.text = title
208
+ p.title.align = 'center'
209
+ p.title.text_font_size = '12pt'
210
+ p.title.text_font_style = 'bold'
211
+
212
+ return p
213
+
214
+ def RhoVolcanoPlot(
215
+ self,
216
+ x_source='rho_score', y_source='-log10(rho_pvalue)',
217
+ xaxis_label='phenotype score',
218
+ yaxis_label='-log10(p-value)',
219
+ up_hit='resistance_hit', down_hit='sensitivity_hit',
220
+ hit_label_col='rho_label',
221
+ x_min=-2.5, x_max=2.5, y_min=0, y_max='auto',
222
+ return_html=True,
223
+ **kwargs
224
+ ):
225
+ p = self._plot_scatter(
226
+ x_source, y_source,
227
+ xaxis_label, yaxis_label,
228
+ up_hit, down_hit,
229
+ hit_label_col,
230
+ x_min, x_max, y_min, y_max,
231
+ **kwargs
232
+ )
233
+
234
+ if return_html:
235
+ return self._get_html(p)
236
+
237
+ self.plots.update(
238
+ {'RhoVolcanoPlot': p}
239
+ )
240
+
241
+ def GammaVolcanoPlot(
242
+ self,
243
+ x_source='gamma_score', y_source='-log10(gamma_pvalue)',
244
+ xaxis_label='phenotype score',
245
+ yaxis_label='-log10(p-value)',
246
+ up_hit='up_hit', down_hit='essential_hit',
247
+ hit_label_col='gamma_label',
248
+ x_min=-2.5, x_max=2.5, y_min=0, y_max='auto',
249
+ return_html=True,
250
+ **kwargs
251
+ ):
252
+ p = self._plot_scatter(
253
+ x_source, y_source,
254
+ xaxis_label, yaxis_label,
255
+ up_hit, down_hit,
256
+ hit_label_col,
257
+ x_min, x_max, y_min, y_max,
258
+ **kwargs
259
+ )
260
+
261
+ if return_html:
262
+ return self._get_html(p)
263
+
264
+ self.plots.update(
265
+ {'GammaVolcanoPlot': p}
266
+ )
267
+
268
+ def RhoGammaScatter(
269
+ self,
270
+ x_source='rho_score', y_source='gamma_score',
271
+ xaxis_label='rho score',
272
+ yaxis_label='gamma score',
273
+ up_hit='resistance_hit', down_hit='sensitivity_hit',
274
+ hit_label_col='rho_label',
275
+ x_min=-2.5, x_max=2.5, y_min=-2.5, y_max=2.5,
276
+ return_html=True,
277
+ **kwargs
278
+ ):
279
+ p = self._plot_scatter(
280
+ x_source, y_source,
281
+ xaxis_label, yaxis_label,
282
+ up_hit, down_hit,
283
+ hit_label_col,
284
+ x_min, x_max, y_min, y_max,
285
+ **kwargs
286
+ )
287
+
288
+ if return_html:
289
+ return self._get_html(p)
290
+
291
+ self.plots.update(
292
+ {'GammaRhoScatter': p}
293
+ )
screenpro/load.py ADDED
@@ -0,0 +1,239 @@
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
+ """Load module
7
+
8
+ Functions to load screen datasets and sgRNA library tables.
9
+ """
10
+
11
+ import pickle
12
+ import pandas as pd
13
+
14
+
15
+ def load_cas9_sgRNA_library(library_path, library_type, sep='\t', index_col=0, protospacer_length=19, verbose=True, **args):
16
+ '''Load Cas9 sgRNA library table for single or dual guide design.
17
+ '''
18
+ library = pd.read_csv(
19
+ library_path,
20
+ sep=sep,
21
+ index_col=index_col,
22
+ **args
23
+ )
24
+
25
+ ## Evaluate library table and reformat columns for downstream analysis
26
+ # I would like to name the target column 'target' if it is named 'gene'!
27
+ #TODO: Add option to keep sublibrary column!
28
+
29
+ if library_type == "single_guide_design":
30
+ eval_columns = ['target', 'sgID', 'protospacer', 'sequence']
31
+
32
+ # reformating columns as needed
33
+ if 'gene' in library.columns:
34
+ # rename gene column to target
35
+ library = library.rename(columns={'gene': 'target'})
36
+ if 'sequence' in library.columns and 'protospacer' not in library.columns:
37
+ library.rename(columns={'sequence': 'protospacer'}, inplace=True)
38
+ if 'sgId' in library.columns:
39
+ library.rename(columns={'sgId': 'sgID'}, inplace=True)
40
+
41
+ # Upper case protospacer sequences
42
+ library['protospacer'] = library['protospacer'].str.upper()
43
+
44
+ protospacer_col = 'protospacer'
45
+ in_length = _check_protospacer_length(library, 'protospacer')
46
+ if in_length == protospacer_length:
47
+ pass
48
+ elif in_length > protospacer_length:
49
+ if verbose: print(f"Trimming protospacer sequences in '{protospacer_col}' column.")
50
+ library = _trim_protospacer(
51
+ library, protospacer_col,
52
+ '5prime',
53
+ in_length - protospacer_length
54
+ )
55
+
56
+ elif in_length < protospacer_length:
57
+ raise ValueError(
58
+ f"Input protospacer length for '{protospacer_col}' is less than {protospacer_length}"
59
+ )
60
+
61
+ # write `sequence` column as `protospacer` (after trimming)
62
+ library['sequence'] = library['protospacer']
63
+
64
+ for col in eval_columns:
65
+ if col not in library.columns:
66
+ raise ValueError(f"Column '{col}' not found in library table.")
67
+
68
+ library = library[eval_columns]
69
+
70
+ elif library_type == "dual_guide_design":
71
+ eval_columns = [
72
+ 'target', 'sgID_AB',
73
+ 'sgID_A', 'protospacer_A',
74
+ 'sgID_B', 'protospacer_B',
75
+ 'sequence'
76
+ ]
77
+
78
+ # reformating columns as needed
79
+ if 'gene' in library.columns:
80
+ # rename gene column to target
81
+ library = library.rename(columns={'gene': 'target'})
82
+
83
+ # Upper case protospacer sequences
84
+ library['protospacer_A'] = library['protospacer_A'].str.upper()
85
+ library['protospacer_B'] = library['protospacer_B'].str.upper()
86
+
87
+ # # TODO: Enable trimming of protospacer sequences through command line arguments.
88
+ for protospacer_col in ['protospacer_A', 'protospacer_B']:
89
+ in_length = _check_protospacer_length(library, protospacer_col)
90
+ if in_length == protospacer_length:
91
+ pass
92
+ elif in_length > protospacer_length:
93
+ if verbose: print(f"Trimming protospacer sequences in '{protospacer_col}' column.")
94
+ library = _trim_protospacer(
95
+ library, protospacer_col,
96
+ '5prime',
97
+ in_length - protospacer_length
98
+ )
99
+
100
+ elif in_length < protospacer_length:
101
+ raise ValueError(
102
+ f"Input protospacer length for '{protospacer_col}' is less than {protospacer_length}"
103
+ )
104
+
105
+ # write `sequence` column as `protospacer_A;protospacer_B` (after trimming)
106
+ library['sequence'] = library['protospacer_A'] + ';' + library['protospacer_B']
107
+
108
+ for col in eval_columns:
109
+ if col not in library.columns:
110
+ raise ValueError(f"Column '{col}' not found in library table.")
111
+
112
+ library = library[eval_columns]
113
+
114
+ else:
115
+ raise ValueError(f"Invalid library type: {library_type}. Please choose 'single_guide_design' or 'dual_guide_design'.")
116
+
117
+ if verbose: print("Library table successfully loaded.")
118
+
119
+ return library
120
+
121
+
122
+ def loadScreenProcessingData(experimentName, collapsedToTranscripts=True, premergedCounts=False):
123
+ """
124
+ Load ScreenProcessing outputs
125
+ (see original code `here <https://github.com/mhorlbeck/ScreenProcessing/blob/master/screen_analysis.py#L70>`__)
126
+ Input files:
127
+ * `*_librarytable.txt` => library table
128
+ * `*_mergedcountstable.txt` => merged counts table
129
+ * `*_phenotypetable.txt` => phenotype table
130
+
131
+ Parameters:
132
+ experimentName (str): name of the experiment
133
+ collapsedToTranscripts (bool): whether the gene scores are collapsed to transcripts
134
+ premergedCounts (bool): whether the counts are premerged
135
+
136
+ Returns:
137
+ dict: dictionary of dataframes
138
+ """
139
+ # dict of dataframes
140
+ dataDict = {
141
+ 'library': pd.read_csv(
142
+ experimentName + '_librarytable.txt',
143
+ sep='\t',
144
+ header=0,
145
+ index_col=0
146
+ ),
147
+ 'counts': pd.read_csv(
148
+ experimentName + '_mergedcountstable.txt',
149
+ sep='\t',
150
+ header=list(range(2)),
151
+ index_col=list(range(1))
152
+ ),
153
+ 'phenotypes': pd.read_csv(
154
+ experimentName + '_phenotypetable.txt',
155
+ sep='\t',
156
+ header=list(range(2)),
157
+ index_col=list(range(1))
158
+ )
159
+ }
160
+
161
+ if premergedCounts:
162
+ # add premerged counts
163
+ dataDict['premerged counts'] = pd.read_csv(
164
+ experimentName + '_rawcountstable.txt',
165
+ sep='\t',
166
+ header=list(range(3)),
167
+ index_col=list(range(1))
168
+ )
169
+
170
+ if collapsedToTranscripts:
171
+ # add transcript scores
172
+ dataDict['transcript scores'] = pd.read_csv(
173
+ experimentName + '_genetable.txt',
174
+ sep='\t',
175
+ header=list(range(3)),
176
+ index_col=list(range(2))
177
+ )
178
+ dataDict['gene scores'] = pd.read_csv(
179
+ experimentName + '_genetable_collapsed.txt',
180
+ sep='\t',
181
+ header=list(range(3)),
182
+ index_col=list(range(1))
183
+ )
184
+ else:
185
+ # add gene scores
186
+ dataDict['gene scores'] = pd.read_csv(
187
+ experimentName + '_genetable.txt',
188
+ sep='\t',
189
+ header=list(range(3)),
190
+ index_col=list(range(1))
191
+ )
192
+
193
+ return dataDict
194
+
195
+
196
+ def _check_protospacer_length(library, protospacer_col):
197
+ lengths = list(set(library[protospacer_col].str.len()))
198
+ if len(lengths) > 1:
199
+ raise ValueError(f"Protospacer lengths are not uniform: {lengths}")
200
+ else:
201
+ length = lengths[0]
202
+ return length
203
+
204
+
205
+ def _trim_protospacer(library, protospacer_col, trim_side, trim_len):
206
+ if trim_side == '5prime':
207
+ library[protospacer_col] = library[protospacer_col].str[trim_len:].str.upper()
208
+
209
+ elif trim_side == '3prime':
210
+ library[protospacer_col] = library[protospacer_col].str[:-trim_len].str.upper()
211
+
212
+ return library
213
+
214
+
215
+ def _write_screen_pkl(screen, name):
216
+ """
217
+ Write AnnData object to a pickle file
218
+
219
+ Parameters:
220
+ screen (object): ScreenPro object to save
221
+ name (str): name of the output file (.pkl extension will be added)
222
+ """
223
+ file_name = f'{name}.pkl'
224
+ with open(file_name, 'wb') as file:
225
+ pickle.dump(screen, file)
226
+ print(f'Object successfully saved to "{file_name}"')
227
+
228
+
229
+ def _read_screen_pkl(name):
230
+ """
231
+ Read ScreenPro object from a pickle file
232
+
233
+ Parameters:
234
+ name (str): name of the input file (.pkl extension will be added)
235
+ """
236
+ file_name = f'{name}.pkl'
237
+ with open(file_name, 'rb') as f:
238
+ screen = pickle.load(f)
239
+ return screen