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,9 @@
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
+ import numpy as np
7
+ import scanpy as sc
8
+ from .qc_plots import plotReplicateScatter, plotCountDistribution
9
+ from .pheno_plots import volcano_plot, label_by_color, label_resistance_hit, label_sensitivity_hit
@@ -0,0 +1,88 @@
1
+ import pandas as pd
2
+ import matplotlib.pyplot as plt
3
+
4
+ from adjustText import adjust_text
5
+ from ._utils import yellow_blue
6
+
7
+
8
+ def rank_plot(df, rank_col, color_col=None, name_col='target', highlight_values_dict=None, xlabel='Rank', ylabel='Values', title='Rank Plot', ax=None, dot_size=1.5, highlight_size_factor=100, txt_font_size=8, **args):
9
+ """
10
+ Plot the ranks against their values with specified color.
11
+
12
+ Args:
13
+ df (DataFrame): The input DataFrame.
14
+ rank_col (str): The column name containing the values to be ranked.
15
+ color_col (str): The column name containing the values to be used for color coding. Default is None.
16
+ name_col (str, optional): The column name containing the names of the values. Default is 'target'.
17
+ highlight_values_dict (dict, optional): A dictionary specifying the values to be highlighted.
18
+ The keys are the highlight colors and the values are dictionaries with 'genes' and 'text' keys.
19
+ 'genes' is a list of values to be highlighted and 'text' is a boolean indicating whether to display
20
+ the names of the highlighted values. Default is None.
21
+ xlabel (str, optional): The label for the x-axis. Default is 'Rank'.
22
+ ylabel (str, optional): The label for the y-axis. Default is 'Values'.
23
+ title (str, optional): The title of the plot. Default is 'Rank Plot'.
24
+ ax (matplotlib.axes.Axes, optional): The axis object to plot on. If not provided, a new axis will be created.
25
+ dot_size (float, optional): The size of the dots in the scatter plot. Default is 1.5.
26
+ highlight_size_factor (int, optional): The size factor for the highlighted dots. Default is 100.
27
+ txt_font_size (int, optional): The font size for the text labels. Default is 8.
28
+ **args: Additional keyword arguments to be passed to the scatter plot.
29
+
30
+ Returns:
31
+ matplotlib.axes.Axes: The axis object containing the plot.
32
+ """
33
+ # Create a new DataFrame with the values and their corresponding ranks
34
+ rank_df = df.copy()
35
+ rank_df['Rank'] = rank_df[rank_col].rank()
36
+ rank_df.sort_values('Rank', inplace=True)
37
+
38
+ # Use a color that is suitable for publications
39
+ if color_col is None:
40
+ color_col = 'darkgray'
41
+
42
+ # If no axis is provided, create one
43
+ if ax is None:
44
+ _, ax = plt.subplots()
45
+
46
+ # Plot the ranks against their values with specified color
47
+ rank_df.plot.scatter(
48
+ 'Rank', rank_col, marker='o',
49
+ colormap=yellow_blue,
50
+ s=dot_size,
51
+ c=color_col, ax=ax,
52
+ colorbar=False,
53
+ **args
54
+ )
55
+
56
+ if highlight_values_dict is not None:
57
+ for highlight_color, highlight_values in highlight_values_dict.items():
58
+ highlight_ranks = rank_df[rank_df[name_col].isin(highlight_values['genes'])]
59
+ ax.plot(highlight_ranks['Rank'], highlight_ranks[rank_col], 'o', color=highlight_color, markersize=dot_size * highlight_size_factor)
60
+
61
+ if highlight_values['text'] is not False:
62
+ texts = []
63
+ for i, row in highlight_ranks.iterrows():
64
+ t = ax.text(
65
+ row['Rank'] + .01,
66
+ row[rank_col] + .001,
67
+ row[name_col],
68
+ fontsize=txt_font_size,
69
+ color=highlight_color,
70
+ ha='right'
71
+ )
72
+ texts.append(t)
73
+
74
+ adjust_text(
75
+ texts,
76
+ arrowprops=dict(arrowstyle='-', color=highlight_color, lw=0.5),
77
+ ax=ax
78
+ )
79
+
80
+ # Add labels and title
81
+ ax.set_xlabel(xlabel)
82
+ ax.set_ylabel(ylabel)
83
+ ax.set_title(title)
84
+
85
+ # Customize the grid lines for a clean look
86
+ ax.grid(False)
87
+
88
+ return rank_df, ax
@@ -0,0 +1,81 @@
1
+ import matplotlib.pyplot as plt
2
+ import matplotlib
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ # variables
7
+ almost_black = '#111111'
8
+ dark2 = ['#1b9e77',
9
+ '#d95f02',
10
+ '#7570b3',
11
+ '#e7298a',
12
+ '#66a61e',
13
+ '#e6ab02',
14
+ '#a6761d',
15
+ '#666666']
16
+ blue_yellow = matplotlib.colors.LinearSegmentedColormap.from_list(
17
+ 'BuYl', [(0, '#ffff00'), (.49, '#000000'), (.51, '#000000'), (1, '#0000ff')])
18
+ blue_yellow.set_bad('#999999', 1)
19
+ yellow_blue = matplotlib.colors.LinearSegmentedColormap.from_list(
20
+ 'YlBu', [(0, '#0000ff'), (.49, '#000000'), (.51, '#000000'), (1, '#ffff00')])
21
+ yellow_blue.set_bad('#999999', 1)
22
+
23
+
24
+ # plt.rcParams['font.sans-serif'] = [
25
+ # 'Helvetica', 'Arial', 'Verdana', 'Bitstream Vera Sans'
26
+ # ]
27
+ # plt.rcParams['font.size'] = 8
28
+ # plt.rcParams['font.weight'] = 'regular'
29
+ # plt.rcParams['text.color'] = almost_black
30
+ #
31
+ # axisLineWidth = .5
32
+ # plt.rcParams['axes.linewidth'] = axisLineWidth
33
+ # plt.rcParams['lines.linewidth'] = 1.5
34
+ #
35
+ # plt.rcParams['axes.facecolor'] = 'white'
36
+ # plt.rcParams['axes.edgecolor'] = almost_black
37
+ # plt.rcParams['axes.labelcolor'] = almost_black
38
+ # # plt.rcParams['axes.color_cycle'] = dark2_all
39
+ #
40
+ # plt.rcParams['patch.edgecolor'] = 'none'
41
+ # plt.rcParams['patch.linewidth'] = .25
42
+ # # plt.rcParams['patch.facecolor'] = dark2_all[0]
43
+ #
44
+ # plt.rcParams['savefig.dpi'] = 1000
45
+ # plt.rcParams['savefig.format'] = 'svg'
46
+ #
47
+ # plt.rcParams['legend.frameon'] = False
48
+ # plt.rcParams['legend.handletextpad'] = .25
49
+ # plt.rcParams['legend.fontsize'] = 8
50
+ # plt.rcParams['legend.numpoints'] = 1
51
+ # plt.rcParams['legend.scatterpoints'] = 1
52
+ #
53
+ # plt.rcParams['ytick.direction'] = 'out'
54
+ # plt.rcParams['ytick.color'] = almost_black
55
+ # plt.rcParams['ytick.major.width'] = axisLineWidth
56
+ # plt.rcParams['xtick.direction'] = 'out'
57
+ # plt.rcParams['xtick.color'] = almost_black
58
+ # plt.rcParams['xtick.major.width'] = axisLineWidth
59
+
60
+
61
+ def cleanAxes(ax, top=False, right=False, bottom=True, left=True):
62
+ ax.grid('off')
63
+ ax.spines['top'].set_visible(top)
64
+ ax.spines['right'].set_visible(right)
65
+ ax.spines['left'].set_visible(left)
66
+ ax.spines['bottom'].set_visible(bottom)
67
+
68
+ #turn off all ticks
69
+ ax.yaxis.set_ticks_position('none')
70
+ ax.xaxis.set_ticks_position('none')
71
+
72
+ #now re-enable visibles
73
+ if top:
74
+ ax.xaxis.tick_top()
75
+ if bottom:
76
+ ax.xaxis.tick_bottom()
77
+ if left:
78
+ ax.yaxis.tick_left()
79
+ if right:
80
+ ax.yaxis.tick_right()
81
+
@@ -0,0 +1,196 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import matplotlib.pyplot as plt
4
+ from ._utils import yellow_blue
5
+ from ._rank import rank_plot
6
+
7
+
8
+ def volcano_plot(
9
+ ax, df, up_hit, down_hit,
10
+ score_col='score', pvalue_col='pvalue',
11
+ xlabel='phenotype score',
12
+ ylabel='-log10(pvalue)',
13
+ dot_size=1,
14
+ xlims='auto',
15
+ ylims='auto',
16
+ ctrl_label='negative_control',
17
+ **args
18
+ ):
19
+
20
+ if f'-log10({pvalue_col})' not in df.columns:
21
+ df[f'-log10({pvalue_col})'] = np.log10(df[pvalue_col]) * -1
22
+
23
+ if xlims == 'auto':
24
+ xlims = (df[score_col].min() - 0.1, df[score_col].max() + 0.1)
25
+ if ylims == 'auto':
26
+ ylims = (df[f'-log10({pvalue_col})'].min() - 0.1, df[f'-log10({pvalue_col})'].max() + 0.1)
27
+
28
+ # Scatter plot for each category
29
+ ax.scatter( df.loc[df['label'] == 'target_non_hit', score_col],
30
+ df.loc[df['label'] == 'target_non_hit', f'-log10({pvalue_col})'],
31
+ alpha=0.1, s=dot_size, c='black', label='target_non_hit',
32
+ **args)
33
+
34
+ ax.scatter( df.loc[df['label'] == up_hit, score_col],
35
+ df.loc[df['label'] == up_hit, f'-log10({pvalue_col})'],
36
+ alpha=0.9, s=dot_size, c='#fcae91', label=up_hit,
37
+ **args)
38
+
39
+ ax.scatter( df.loc[df['label'] == down_hit, score_col],
40
+ df.loc[df['label'] == down_hit, f'-log10({pvalue_col})'],
41
+ alpha=0.9, s=dot_size, c='#bdd7e7', label=down_hit,
42
+ **args)
43
+
44
+ ax.scatter( df.loc[df['label'] == ctrl_label, score_col],
45
+ df.loc[df['label'] == ctrl_label, f'-log10({pvalue_col})'],
46
+ alpha=0.1, s=dot_size, c='gray', label=ctrl_label,
47
+ **args)
48
+
49
+ # Set x-axis and y-axis labels
50
+ ax.set_xlabel(xlabel)
51
+ ax.set_ylabel(ylabel)
52
+
53
+ # Set x-axis limits
54
+ ax.set_xlim(xlims)
55
+
56
+ # Set y-axis limits
57
+ ax.set_ylim(ylims)
58
+
59
+ # Add legend
60
+ ax.legend()
61
+
62
+
63
+ def label_by_color(ax, df_in, label,
64
+ x_col, y_col,
65
+ size=2, size_txt="auto",
66
+ edgecolors='black', facecolors='black',
67
+ textcolor='black',
68
+ t_x=.5, t_y=-0.1, **args):
69
+
70
+ if size_txt == 'auto': size_txt = size * 2
71
+
72
+ df = df_in.copy()
73
+ target_data = df[df['target'] == label]
74
+
75
+ # Scatter plot for labeled data
76
+ ax.scatter(
77
+ target_data[x_col], target_data[y_col],
78
+ s=size, linewidth=0.5,
79
+ edgecolors=edgecolors,
80
+ facecolors=facecolors, label='target',
81
+ **args
82
+ )
83
+
84
+ if size_txt == None:
85
+ pass
86
+ else:
87
+ # Annotate the points
88
+ for i, _ in enumerate(target_data['target']):
89
+ txt = target_data['target'].iloc[i]
90
+ ax.annotate(txt, (target_data[x_col].iloc[i] + t_x, target_data[y_col].iloc[i] + t_y),
91
+ color=textcolor, size=size_txt)
92
+
93
+
94
+ def label_as_black(ax, df_in, label,
95
+ x_col='score', y_col='-log10(pvalue)',
96
+ size=2, size_txt="auto",
97
+ t_x=.5, t_y=-0.1,
98
+ **args):
99
+ label_by_color(
100
+ ax, df_in, label,
101
+ x_col=x_col, y_col=y_col,
102
+ size=size, size_txt=size_txt,
103
+ edgecolors='black', facecolors='black',
104
+ textcolor='black',
105
+ t_x=t_x, t_y=t_y,
106
+ **args)
107
+
108
+
109
+ def label_sensitivity_hit(ax, df_in, label,
110
+ x_col='score', y_col='-log10(pvalue)',
111
+ size=2, size_txt="auto",
112
+ t_x=.5, t_y=-0.1,
113
+ **args):
114
+ label_by_color(
115
+ ax, df_in, label,
116
+ x_col=x_col, y_col=y_col,
117
+ size=size, size_txt=size_txt,
118
+ edgecolors='black', facecolors='#3182bd',
119
+ textcolor='black',
120
+ t_x=t_x, t_y=t_y,
121
+ **args)
122
+
123
+
124
+ def label_resistance_hit(ax, df_in, label,
125
+ x_col='score', y_col='-log10(pvalue)',
126
+ size=2, size_txt="auto",
127
+ t_x=.5, t_y=-0.1,
128
+ **args):
129
+ label_by_color(
130
+ ax, df_in, label,
131
+ x_col=x_col, y_col=y_col,
132
+ size=size, size_txt=size_txt,
133
+ edgecolors='black', facecolors='#de2d26',
134
+ textcolor='black',
135
+ t_x=t_x, t_y=t_y,
136
+ **args)
137
+
138
+
139
+ # def drawRhoGammaScatter(
140
+ # self, ax,
141
+ # rho_df=None, gamma_df=None,
142
+ # dot_size=1,
143
+ # score_col='score',
144
+ # xlabel='auto',
145
+ # ylabel='auto',
146
+ # xlims='auto',
147
+ # ylims='auto',
148
+ # **args
149
+ # ):
150
+ # #TODO: fix by making a single dataframe with both rho and gamma scores
151
+ # if rho_df is None:
152
+ # _, _, rho_df = self._prep_data()
153
+ # if gamma_df is None:
154
+ # gamma_df, _, _ = self._prep_data()
155
+
156
+ # if xlabel == 'auto':
157
+ # xlabel = self.rho_score_name.replace(':', ': ').replace('_', ' ')
158
+ # if ylabel == 'auto':
159
+ # ylabel = self.gamma_score_name.replace(':', ': ').replace('_', ' ')
160
+
161
+ # # color by rho score labels
162
+ # up_hit = 'resistance_hit'
163
+ # down_hit = 'sensitivity_hit'
164
+
165
+ # # Scatter plot for each category
166
+ # ax.scatter( rho_df.loc[rho_df['label'] == 'target_non_hit', score_col],
167
+ # gamma_df.loc[rho_df['label'] == 'target_non_hit', score_col],
168
+ # alpha=0.1, s=dot_size, c='black', label='target_non_hit',
169
+ # **args)
170
+
171
+ # ax.scatter( rho_df.loc[rho_df['label'] == up_hit, score_col],
172
+ # gamma_df.loc[rho_df['label'] == up_hit, score_col],
173
+ # alpha=0.9, s=dot_size, c='#fcae91', label=up_hit,
174
+ # **args)
175
+
176
+ # ax.scatter( rho_df.loc[rho_df['label'] == down_hit, score_col],
177
+ # gamma_df.loc[rho_df['label'] == down_hit, score_col],
178
+ # alpha=0.9, s=dot_size, c='#bdd7e7', label=down_hit,
179
+ # **args)
180
+
181
+ # ax.scatter( rho_df.loc[rho_df['label'] == self.ctrl_label, score_col],
182
+ # gamma_df.loc[rho_df['label'] == self.ctrl_label, score_col],
183
+ # alpha=0.1, s=dot_size, c='gray', label=self.ctrl_label,
184
+ # **args)
185
+
186
+ # # Set x-axis and y-axis labels
187
+ # ax.set_xlabel(xlabel)
188
+ # ax.set_ylabel(ylabel)
189
+
190
+ # # Set x-axis limits
191
+ # ax.set_xlim(xlims)
192
+ # ax.set_ylim(ylims)
193
+
194
+ # # Add legend
195
+ # ax.legend()
196
+
@@ -0,0 +1,45 @@
1
+ import numpy as np
2
+ import scanpy as sc
3
+ from ._utils import almost_black
4
+
5
+
6
+ ## Histogram of guide counts distribution
7
+ def plotCountDistribution(ax, adat, title, **args):
8
+ pass
9
+
10
+
11
+ ## Scatter plot of replicates
12
+ def plotReplicateScatter(ax, adat_in, x, y, title, min_val=None, max_val=None, log_transform=True, **args):
13
+ adat = adat_in[[x, y], :].copy()
14
+
15
+ adat.obs.index = [f'Replicate {str(r)}' for r in adat.obs.replicate.to_list()]
16
+ x_lab, y_lab = [f'Replicate {str(r)}' for r in adat.obs.replicate.to_list()]
17
+
18
+ if log_transform:
19
+ adat.X = np.log10(adat.X+1)
20
+
21
+ if min_val is None:
22
+ min_val = min([adat.to_df().loc[x_lab,:].min(), adat.to_df().loc[y_lab,:].min()])
23
+ min_val = min_val * 1.1
24
+ if max_val is None:
25
+ max_val = max([adat.to_df().loc[x_lab,:].max(), adat.to_df().loc[y_lab,:].max()])
26
+ max_val = max_val * 1.1
27
+
28
+ sc.pl.scatter(
29
+ adat,
30
+ x_lab, y_lab,
31
+ legend_fontsize='xx-large',
32
+ palette=[almost_black, '#BFBFBF'],
33
+ color='targetType',
34
+ title=title,
35
+ size=5,
36
+ show=False,
37
+ ax=ax,
38
+ **args
39
+ )
40
+ ax.set_ylim(min_val, max_val)
41
+ ax.set_xlim(min_val, max_val)
42
+ ax.tick_params(axis='both', labelsize=10)
43
+ ax.get_legend().remove()
44
+
45
+ ax.grid(False)
@@ -0,0 +1,98 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ from pydeseq2 import preprocessing
4
+
5
+
6
+ def findLowCounts(adata, filter_type, minimum_reads, verbose=True):
7
+ """
8
+ Label variables with low counts in either or all samples.
9
+
10
+ Parameters:
11
+ adata (AnnData): AnnData object containing the counts to be filtered.
12
+ filter_type (str): specify the filter type. Possible values are: 'all', or 'sum'.
13
+ minimum_reads (int): minimum number of reads.
14
+ verbose (bool): print the number of removed variables. Default is True.
15
+
16
+ Returns:
17
+ None
18
+ """
19
+ count_bin = adata.X >= minimum_reads
20
+
21
+ if filter_type == 'all':
22
+ out = adata[:, count_bin.all(axis=0)].copy()
23
+ elif filter_type == 'sum':
24
+ out = adata[:, adata.to_df().sum(axis=0) >= minimum_reads].copy()
25
+ else:
26
+ raise ValueError(f'filter_type "{filter_type}" not recognized. Use "all", or "sum".')
27
+
28
+ if verbose:
29
+ n_removed = adata.shape[1] - out.shape[1]
30
+ # print the number of removed variables
31
+ print(
32
+ f"{n_removed} variables with less than {minimum_reads} reads (filter_type: '{filter_type}')"
33
+ )
34
+
35
+ adata.var['low_count'] = ~adata.var.index.isin(out.var.index.to_list())
36
+
37
+
38
+ def addPseudoCount(adata, behavior, value, inplace=True):
39
+ """
40
+ Add pseudocounts to the given counts based on the specified behavior.
41
+
42
+ Args:
43
+ adata (AnnData): AnnData object containing the counts to which pseudocounts will be added.
44
+ behavior (str): The behavior for adding pseudocounts. Possible values are:
45
+ - 'default' or 'zeros_only': Add pseudocounts only to rows with at least one zero value.
46
+ - 'all_values': Add pseudocounts to all rows.
47
+ - 'filter_out': Set rows with at least one zero value to NaN.
48
+ value (float): The value of the pseudocount to be added.
49
+ inplace (bool): If True, the pseudocounts will replace the original counts in the AnnData object.
50
+
51
+ Returns:
52
+ DataFrame: The counts DataFrame with pseudocounts added based on the specified behavior.
53
+
54
+ Raises:
55
+ ValueError: If the pseudocount behavior is not recognized or not implemented.
56
+ """
57
+ ## possible pseudocount behaviors
58
+ # 1. remove 0
59
+ # 2. add pseudocount
60
+ # 3. impute 0 (it's hard)
61
+
62
+ # Source:
63
+ # https://github.com/mhorlbeck/ScreenProcessing/blob/0ee5192ecc17348665bd1387ddfa9037efb7964f/process_experiments.py#L485
64
+
65
+ counts = adata.to_df()
66
+
67
+ # pseudocount
68
+ if behavior == 'default' or behavior == 'zeros_only':
69
+ counts_pseudo = counts.replace(0, value)
70
+
71
+ elif behavior == 'all_values':
72
+ counts_pseudo = counts + value
73
+ elif behavior == 'filter_out':
74
+ counts_pseudo = counts.replace(0, np.nan)
75
+ else:
76
+ raise ValueError(
77
+ 'Pseudocount behavior not recognized or not implemented')
78
+
79
+ if inplace:
80
+ adata.X = counts_pseudo.to_numpy()
81
+ else:
82
+ return counts_pseudo
83
+
84
+
85
+ def normalizeSeqDepth(adata):
86
+ """
87
+ Normalize counts by sequencing depth and update the adata object.
88
+ This function uses the PyDESeq2 normalization method.
89
+
90
+ Args:
91
+ adata (AnnData): AnnData object containing the counts to be normalized.
92
+ """
93
+ # normalize counts by sequencing depth
94
+ norm_counts, size_factors = preprocessing.deseq2_norm(adata.X)
95
+ # update adata object
96
+ adata.obs['size_factors'] = size_factors
97
+ adata.layers['seq_depth_norm'] = norm_counts
98
+ adata.X = adata.layers['seq_depth_norm']
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022-2024 ScreenPro2 Development Team.
4
+ All rights reserved.
5
+ Gilbart Lab, UCSF / Arc Institute.
6
+ Multi-Omics Tech Center, Arc Insititue.
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+