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 +30 -0
- screenpro/__init__.py +31 -0
- screenpro/__main__.py +8 -0
- screenpro/assays/__init__.py +465 -0
- screenpro/dashboard/__init__.py +293 -0
- screenpro/load.py +239 -0
- screenpro/main.py +227 -0
- screenpro/ngs/__init__.py +390 -0
- screenpro/ngs/cas12.py +206 -0
- screenpro/ngs/cas9.py +276 -0
- screenpro/phenoscore/__init__.py +148 -0
- screenpro/phenoscore/_annotate.py +122 -0
- screenpro/phenoscore/delta.py +375 -0
- screenpro/phenoscore/deseq.py +57 -0
- screenpro/phenoscore/evaluate.py +66 -0
- screenpro/phenoscore/phenostat.py +84 -0
- screenpro/plotting/__init__.py +9 -0
- screenpro/plotting/_rank.py +88 -0
- screenpro/plotting/_utils.py +81 -0
- screenpro/plotting/pheno_plots.py +196 -0
- screenpro/plotting/qc_plots.py +45 -0
- screenpro/preprocessing.py +98 -0
- screenpro2-0.5.0.dist-info/LICENSE +25 -0
- screenpro2-0.5.0.dist-info/METADATA +366 -0
- screenpro2-0.5.0.dist-info/RECORD +27 -0
- screenpro2-0.5.0.dist-info/WHEEL +4 -0
- screenpro2-0.5.0.dist-info/entry_points.txt +3 -0
screenpro/ngs/cas12.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import biobear as bb
|
|
2
|
+
import polars as pl
|
|
3
|
+
from time import time
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def fastq_to_count_merged_reads(
|
|
7
|
+
fastq_file_path:str,
|
|
8
|
+
verbose: bool=False) -> pl.DataFrame:
|
|
9
|
+
if verbose: ('count unique sequences ...')
|
|
10
|
+
t0 = time()
|
|
11
|
+
|
|
12
|
+
session = bb.connect()
|
|
13
|
+
|
|
14
|
+
sql_cmd = f"""
|
|
15
|
+
SELECT f.sequence AS sequence, COUNT(*) as count
|
|
16
|
+
FROM fastq_scan('{fastq_file_path}') f
|
|
17
|
+
GROUP BY sequence
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
df_count = session.sql(sql_cmd).to_polars()
|
|
21
|
+
|
|
22
|
+
if verbose: print("done in %0.3fs" % (time() - t0))
|
|
23
|
+
|
|
24
|
+
return df_count
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_spacers_cas12(df_count,DRref):
|
|
28
|
+
|
|
29
|
+
# get 1st spacer sequence
|
|
30
|
+
df_count = df_count.with_columns(
|
|
31
|
+
DR1_loc = df_count['sequence'].str.find(DRref['DR-1']),
|
|
32
|
+
).with_columns(
|
|
33
|
+
pl.col('sequence').str.slice(
|
|
34
|
+
pl.col("DR1_loc")-23, 23
|
|
35
|
+
).alias("SP1_sequence")
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# get 2ed+ spacer sequence
|
|
39
|
+
|
|
40
|
+
for DR_key, DR_seq in DRref.items():
|
|
41
|
+
DR_n = int(DR_key[-1]) # get the number of the DR
|
|
42
|
+
spacer_i = DR_n+1 # get the spacer number, 3' of the DR sequence
|
|
43
|
+
df_count = df_count.with_columns(
|
|
44
|
+
df_count['sequence'].str.find(DR_seq).alias(f"DR{DR_n}_loc")
|
|
45
|
+
).with_columns(
|
|
46
|
+
pl.col('sequence').str.slice(
|
|
47
|
+
pl.col(f"DR{DR_n}_loc")+19, 23
|
|
48
|
+
).alias(f"SP{spacer_i}_sequence")
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
out = df_count.select([
|
|
52
|
+
f'SP{i}_sequence' for i in range(1,len(DRref)+2)
|
|
53
|
+
]+ ['count']).group_by([
|
|
54
|
+
f'SP{i}_sequence' for i in range(1,len(DRref)+2)
|
|
55
|
+
]).sum()
|
|
56
|
+
|
|
57
|
+
return df_count, out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def map_to_cas12_pairs_library(df_count,library,DR1_seq, get_recombinant=False, verbose=False):
|
|
61
|
+
|
|
62
|
+
t0 = time()
|
|
63
|
+
|
|
64
|
+
df_count, df_count_split = get_spacers_cas12(df_count, DRref = {'DR-1': DR1_seq})
|
|
65
|
+
|
|
66
|
+
if verbose:
|
|
67
|
+
perc_DR1 = df_count.with_columns(
|
|
68
|
+
pl.col('DR1_loc').fill_null(0).gt(0)
|
|
69
|
+
).filter(
|
|
70
|
+
pl.col('DR1_loc')
|
|
71
|
+
).get_column('count').sum() / df_count['count'].sum() * 100
|
|
72
|
+
|
|
73
|
+
print(f"% counts with DR1: {perc_DR1}")
|
|
74
|
+
|
|
75
|
+
df_res = pl.DataFrame(library[['SP1_sequence','SP2_sequence']].reset_index()).join(
|
|
76
|
+
df_count_split,
|
|
77
|
+
on=["SP1_sequence","SP2_sequence"], how="left"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if verbose:
|
|
81
|
+
perc_mapped = df_res['count'].drop_nulls().sum() / df_count['count'].sum() * 100
|
|
82
|
+
|
|
83
|
+
print(f"% counts mapped to library: {perc_mapped}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if get_recombinant:
|
|
87
|
+
df_res_unmap = df_count_split.join(
|
|
88
|
+
pl.DataFrame(
|
|
89
|
+
library[['SP1_sequence','SP2_sequence']].reset_index()
|
|
90
|
+
), on=["SP1_sequence","SP2_sequence"], how="anti"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
df_res_unmap_remapped_sp1 = df_res_unmap.join(
|
|
94
|
+
pl.DataFrame(library[['SP1_name','SP1_id','SP1_sequence']]), on=["SP1_sequence"], how="left"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
df_res_unmap_remapped_sp1_sp2 = df_res_unmap_remapped_sp1.join(
|
|
98
|
+
pl.DataFrame(library[['SP2_name','SP2_id','SP2_sequence']]),
|
|
99
|
+
on=["SP2_sequence"], how="left"
|
|
100
|
+
).drop_nulls().unique().with_columns(
|
|
101
|
+
recombinant_name=pl.concat_str(
|
|
102
|
+
[
|
|
103
|
+
pl.col("SP1_name"),
|
|
104
|
+
pl.col("SP1_id"),
|
|
105
|
+
pl.col("SP2_name"),
|
|
106
|
+
pl.col("SP2_id"),
|
|
107
|
+
],
|
|
108
|
+
separator="_"
|
|
109
|
+
)
|
|
110
|
+
).select(['recombinant_name','SP1_sequence','SP2_sequence','count'])
|
|
111
|
+
|
|
112
|
+
if verbose:
|
|
113
|
+
perc_remapped = df_res_unmap_remapped_sp1_sp2['count'].drop_nulls().sum() / df_count['count'].sum() * 100
|
|
114
|
+
|
|
115
|
+
print(f"% counts remapped to library: {perc_remapped} [fully remapped recombination events]")
|
|
116
|
+
|
|
117
|
+
if verbose: print("done in %0.3fs" % (time() - t0))
|
|
118
|
+
|
|
119
|
+
return df_res, df_res_unmap_remapped_sp1_sp2
|
|
120
|
+
|
|
121
|
+
else:
|
|
122
|
+
if verbose: print("done in %0.3fs" % (time() - t0))
|
|
123
|
+
|
|
124
|
+
return df_res
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def map_to_cas12_triplets_library(df_count,library,DR1_seq, DR2_seq, get_recombinant=False, verbose=False):
|
|
128
|
+
|
|
129
|
+
t0 = time()
|
|
130
|
+
|
|
131
|
+
df_count, df_count_split = get_spacers_cas12(df_count, DRref = {'DR-1': DR1_seq, 'DR-2': DR2_seq})
|
|
132
|
+
|
|
133
|
+
if verbose:
|
|
134
|
+
perc_DR1 = df_count.with_columns(
|
|
135
|
+
pl.col('DR1_loc').fill_null(0).gt(0)
|
|
136
|
+
).filter(
|
|
137
|
+
pl.col('DR1_loc')
|
|
138
|
+
).get_column('count').sum() / df_count['count'].sum() * 100
|
|
139
|
+
|
|
140
|
+
print(f"% counts with DR1: {perc_DR1}")
|
|
141
|
+
|
|
142
|
+
perc_DR2 = df_count.with_columns(
|
|
143
|
+
pl.col('DR2_loc').fill_null(0).gt(0)
|
|
144
|
+
).filter(
|
|
145
|
+
pl.col('DR2_loc')
|
|
146
|
+
).get_column('count').sum() / df_count['count'].sum() * 100
|
|
147
|
+
|
|
148
|
+
print(f"% counts with DR2: {perc_DR2}")
|
|
149
|
+
|
|
150
|
+
df_res = pl.DataFrame(library[['SP1_sequence','SP2_sequence','SP3_sequence']].reset_index()).join(
|
|
151
|
+
df_count_split,
|
|
152
|
+
on=["SP1_sequence","SP2_sequence","SP3_sequence"], how="left"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if verbose:
|
|
156
|
+
perc_mapped = df_res['count'].drop_nulls().sum() / df_count['count'].sum() * 100
|
|
157
|
+
|
|
158
|
+
print(f"% counts mapped to library: {perc_mapped}")
|
|
159
|
+
|
|
160
|
+
if get_recombinant:
|
|
161
|
+
df_res_unmap = df_count_split.join(
|
|
162
|
+
pl.DataFrame(
|
|
163
|
+
library[['SP1_sequence','SP2_sequence','SP3_sequence']].reset_index()
|
|
164
|
+
), on=["SP1_sequence","SP2_sequence","SP3_sequence"], how="anti"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
df_res_unmap_remapped_sp1 = df_res_unmap.join(
|
|
168
|
+
pl.DataFrame(library[['SP1_name','SP1_id','SP1_sequence']]), on=["SP1_sequence"], how="left"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
df_res_unmap_remapped_sp1_sp2 = df_res_unmap_remapped_sp1.join(
|
|
172
|
+
pl.DataFrame(library[['SP2_name','SP2_id','SP2_sequence']]),
|
|
173
|
+
on=["SP2_sequence"], how="left"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
df_res_unmap_remapped_sp1_sp2_sp3 = df_res_unmap_remapped_sp1_sp2.join(
|
|
177
|
+
pl.DataFrame(library[['SP3_name','SP3_id','SP3_sequence']]),
|
|
178
|
+
on=["SP3_sequence"], how="left"
|
|
179
|
+
).drop_nulls().unique().with_columns(
|
|
180
|
+
recombinant_name=pl.concat_str(
|
|
181
|
+
[
|
|
182
|
+
pl.col("SP1_name"),
|
|
183
|
+
pl.col("SP1_id"),
|
|
184
|
+
pl.col("SP2_name"),
|
|
185
|
+
pl.col("SP2_id"),
|
|
186
|
+
pl.col("SP3_name"),
|
|
187
|
+
pl.col("SP3_id"),
|
|
188
|
+
],
|
|
189
|
+
separator="_"
|
|
190
|
+
)
|
|
191
|
+
).select(['recombinant_name','SP1_sequence','SP2_sequence','SP3_sequence','count'])
|
|
192
|
+
|
|
193
|
+
if verbose:
|
|
194
|
+
perc_remapped = df_res_unmap_remapped_sp1_sp2_sp3['count'].drop_nulls().sum() / df_count['count'].sum() * 100
|
|
195
|
+
|
|
196
|
+
print(f"% counts remapped to library: {perc_remapped} [fully remapped recombination events]")
|
|
197
|
+
|
|
198
|
+
if verbose: print("done in %0.3fs" % (time() - t0))
|
|
199
|
+
|
|
200
|
+
return df_res, df_res_unmap_remapped_sp1_sp2_sp3
|
|
201
|
+
|
|
202
|
+
else:
|
|
203
|
+
|
|
204
|
+
if verbose: print("done in %0.3fs" % (time() - t0))
|
|
205
|
+
|
|
206
|
+
return df_res
|
screenpro/ngs/cas9.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
from time import time
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import polars as pl
|
|
4
|
+
import biobear as bb
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def fastq_to_count_single_guide(
|
|
8
|
+
fastq_file_path:str,
|
|
9
|
+
trim5p_start:int=None, trim5p_length:int=None,
|
|
10
|
+
verbose: bool=False) -> pl.DataFrame:
|
|
11
|
+
"""
|
|
12
|
+
Count the occurrences of unique sequences in single-end FASTQ files to a DataFrame containing counts of unique sequences.
|
|
13
|
+
e.g. single-guide design R1: protospacer
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
fastq_file_path (str): The path to the FASTQ file.
|
|
17
|
+
trim5p_start (int, optional): The starting position for trimming the 5' end of the sequences. Defaults to None.
|
|
18
|
+
trim5p_length (int, optional): The length of the trimmed sequences. Defaults to None.
|
|
19
|
+
verbose (bool, optional): Whether to print verbose output. Defaults to False.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
pl.DataFrame: A DataFrame containing the unique sequences and their respective counts.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
if verbose: ('count unique sequences ...')
|
|
26
|
+
t0 = time()
|
|
27
|
+
|
|
28
|
+
session = bb.connect()
|
|
29
|
+
|
|
30
|
+
if trim5p_start and trim5p_length:
|
|
31
|
+
sql_cmd = f"""
|
|
32
|
+
SELECT substr(f.sequence, {trim5p_start}, {trim5p_length}) AS protospacer, COUNT(*) as count
|
|
33
|
+
FROM fastq_scan('{fastq_file_path}') f
|
|
34
|
+
GROUP BY protospacer
|
|
35
|
+
"""
|
|
36
|
+
else:
|
|
37
|
+
sql_cmd = f"""
|
|
38
|
+
SELECT f.sequence AS protospacer, COUNT(*) as count
|
|
39
|
+
FROM fastq_scan('{fastq_file_path}') f
|
|
40
|
+
GROUP BY protospacer
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
df_count = session.sql(sql_cmd).to_polars()
|
|
44
|
+
|
|
45
|
+
if verbose: print("done in %0.3fs" % (time() - t0))
|
|
46
|
+
|
|
47
|
+
return df_count
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fastq_to_count_dual_guide(
|
|
51
|
+
R1_fastq_file_path:str, R2_fastq_file_path:str,
|
|
52
|
+
trim5p_pos1_start:int=None, trim5p_pos1_length:int=None,
|
|
53
|
+
trim5p_pos2_start:int=None, trim5p_pos2_length:int=None,
|
|
54
|
+
verbose: bool=False) -> pl.DataFrame:
|
|
55
|
+
"""
|
|
56
|
+
Count the occurrences of unique sequences in paired-end FASTQ files to a DataFrame containing counts of unique pairs of sequences.
|
|
57
|
+
e.g. dual-guide design R1: protospacer_A, R2: protospacer_B
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
R1_fastq_file_path (str): File path of the R1 FASTQ file.
|
|
61
|
+
R2_fastq_file_path (str): File path of the R2 FASTQ file.
|
|
62
|
+
trim5p_pos1_start (int, optional): Start position for trimming the 5' end of the R1 sequences. Defaults to None.
|
|
63
|
+
trim5p_pos1_length (int, optional): Length of the trimmed R1 sequences. Defaults to None.
|
|
64
|
+
trim5p_pos2_start (int, optional): Start position for trimming the 5' end of the R2 sequences. Defaults to None.
|
|
65
|
+
trim5p_pos2_length (int, optional): Length of the trimmed R2 sequences. Defaults to None.
|
|
66
|
+
verbose (bool, optional): Whether to print verbose output. Defaults to False.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
pl.DataFrame: DataFrame containing counts of unique sequences with columns 'protospacer_A', 'protospacer_B', and 'count'.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If trim5p_pos1_start, trim5p_pos1_length, trim5p_pos2_start, and trim5p_pos2_length are not provided concurrently.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
if verbose: ('count unique sequences ...')
|
|
76
|
+
t0 = time()
|
|
77
|
+
|
|
78
|
+
session = bb.connect()
|
|
79
|
+
|
|
80
|
+
if trim5p_pos1_start and trim5p_pos1_length and trim5p_pos2_start and trim5p_pos2_length:
|
|
81
|
+
sql_cmd = f"""
|
|
82
|
+
WITH pos1 AS (
|
|
83
|
+
SELECT REPLACE(name, '_R1', '') trimmed_name, *
|
|
84
|
+
FROM fastq_scan('{R1_fastq_file_path}')
|
|
85
|
+
), pos2 AS (
|
|
86
|
+
SELECT REPLACE(name, '_R2', '') trimmed_name, *
|
|
87
|
+
FROM fastq_scan('{R2_fastq_file_path}')
|
|
88
|
+
)
|
|
89
|
+
SELECT substr(pos1.sequence, {trim5p_pos1_start}, {trim5p_pos1_length}) protospacer_A, reverse_complement(substr(pos2.sequence, {trim5p_pos2_start}, {trim5p_pos2_length})) protospacer_B, COUNT(*) count
|
|
90
|
+
FROM pos1
|
|
91
|
+
JOIN pos2
|
|
92
|
+
ON pos1.name = pos2.name
|
|
93
|
+
GROUP BY protospacer_A, protospacer_B
|
|
94
|
+
"""
|
|
95
|
+
elif trim5p_pos1_start==None and trim5p_pos1_length==None and trim5p_pos2_start==None and trim5p_pos2_length==None:
|
|
96
|
+
sql_cmd = f"""
|
|
97
|
+
WITH pos1 AS (
|
|
98
|
+
SELECT REPLACE(name, '_R1', '') trimmed_name, *
|
|
99
|
+
FROM fastq_scan('{R1_fastq_file_path}')
|
|
100
|
+
), pos2 AS (
|
|
101
|
+
SELECT REPLACE(name, '_R2', '') trimmed_name, *
|
|
102
|
+
FROM fastq_scan('{R2_fastq_file_path}')
|
|
103
|
+
)
|
|
104
|
+
SELECT pos1.sequence protospacer_A, reverse_complement(pos2.sequence) protospacer_B, COUNT(*) count
|
|
105
|
+
FROM pos1
|
|
106
|
+
JOIN pos2
|
|
107
|
+
ON pos1.name = pos2.name
|
|
108
|
+
GROUP BY protospacer_A, protospacer_B
|
|
109
|
+
"""
|
|
110
|
+
else:
|
|
111
|
+
raise ValueError("trim5p_pos1_start, trim5p_pos1_length, \
|
|
112
|
+
trim5p_pos2_start, and trim5p_pos2_length \
|
|
113
|
+
must be provided concurrently!")
|
|
114
|
+
|
|
115
|
+
df_count = session.sql(sql_cmd).to_polars()
|
|
116
|
+
|
|
117
|
+
if verbose: print("done in %0.3fs" % (time() - t0))
|
|
118
|
+
|
|
119
|
+
return df_count
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def map_to_library_single_guide(df_count, library, return_type='all', verbose=False):
|
|
123
|
+
"""
|
|
124
|
+
Map the counts of unique sequences to a library DataFrame containing sgRNA sequences.
|
|
125
|
+
User can choose to return mapped reads, unmapped reads, or both.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
df_count (pandas.DataFrame): The input DataFrame containing counts.
|
|
129
|
+
library (pandas.DataFrame): The library DataFrame to map to.
|
|
130
|
+
return_type (str, optional): The type of result to return. Defaults to 'all'.
|
|
131
|
+
verbose (bool, optional): Whether to print verbose information. Defaults to False.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
dict or pandas.DataFrame: The mapped result based on the return_type parameter.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: If the return_type parameter is invalid.
|
|
138
|
+
"""
|
|
139
|
+
# get counts for given input
|
|
140
|
+
res = df_count.clone() #cheap deepcopy/clone
|
|
141
|
+
res = res.sort('count', descending=True)
|
|
142
|
+
|
|
143
|
+
res = res.with_columns(
|
|
144
|
+
pl.col("protospacer").alias("sequence"),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
res_map = pl.DataFrame(library).join(
|
|
148
|
+
res, on="sequence", how="left"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if return_type == 'unmapped' or return_type == 'all':
|
|
152
|
+
res_unmap = res.join(
|
|
153
|
+
pl.DataFrame(library), on="sequence", how="anti"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if verbose:
|
|
157
|
+
print("% mapped reads",
|
|
158
|
+
100 * \
|
|
159
|
+
res_map.to_pandas()['count'].fillna(0).sum() / \
|
|
160
|
+
int(res.select(pl.sum("count")).to_pandas()['count'])
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if return_type == 'unmapped':
|
|
164
|
+
return res_unmap
|
|
165
|
+
elif return_type == 'mapped':
|
|
166
|
+
return res_map
|
|
167
|
+
elif return_type == 'all':
|
|
168
|
+
return {'full': res, 'mapped': res_map, 'unmapped': res_unmap}
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError("return_type must be either 'unmapped', 'mapped', or 'all'")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def map_to_library_dual_guide(df_count, library, get_recombinant=False, return_type='all', verbose=False):
|
|
174
|
+
"""
|
|
175
|
+
Map the counts of unique sequences to a library DataFrame containing dual-guide sgRNA sequences.
|
|
176
|
+
Optionally, the function can capture recombinant events.
|
|
177
|
+
User can choose to return mapped reads, unmapped reads, recombinant events, or all.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
df_count (pandas.DataFrame): The input DataFrame containing the counts.
|
|
181
|
+
library (pandas.DataFrame): The library of sequences to map against.
|
|
182
|
+
get_recombinant (bool, optional): Whether to calculate recombinant events. Defaults to False.
|
|
183
|
+
return_type (str, optional): The type of reads to return. Can be 'unmapped', 'mapped', 'recombinant', or 'all'. Defaults to 'all'.
|
|
184
|
+
verbose (bool, optional): Whether to print verbose output. Defaults to False.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
pandas.DataFrame or dict: The mapped reads based on the specified return_type.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ValueError: If return_type is not one of 'unmapped', 'mapped', 'recombinant', or 'all'.
|
|
191
|
+
ValueError: If get_recombinant is False and return_type is 'recombinant'.
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
# get counts for given input
|
|
195
|
+
res = df_count.clone() #cheap deepcopy/clone
|
|
196
|
+
res = res.rename(
|
|
197
|
+
{'protospacer_a':'protospacer_A','protospacer_b':'protospacer_B'}
|
|
198
|
+
)
|
|
199
|
+
res = res.sort('count', descending=True)
|
|
200
|
+
res = res.with_columns(
|
|
201
|
+
pl.concat_str(
|
|
202
|
+
[
|
|
203
|
+
pl.col("protospacer_A"),
|
|
204
|
+
pl.col("protospacer_B")
|
|
205
|
+
],
|
|
206
|
+
separator=";",
|
|
207
|
+
).alias("sequence"),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# map to library
|
|
211
|
+
res_map = pl.DataFrame(library).join(
|
|
212
|
+
res, on="sequence", how="left"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# get unmapped reads to the library
|
|
216
|
+
if get_recombinant or return_type == 'unmapped' or return_type == 'all':
|
|
217
|
+
res_unmap = res.join(
|
|
218
|
+
pl.DataFrame(library), on="sequence", how="anti"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if verbose:
|
|
222
|
+
print("% mapped reads",
|
|
223
|
+
100 * \
|
|
224
|
+
res_map.to_pandas()['count'].fillna(0).sum() / \
|
|
225
|
+
int(res.select(pl.sum("count")).to_pandas()['count'])
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if get_recombinant:
|
|
229
|
+
|
|
230
|
+
if verbose:
|
|
231
|
+
print("% unmapped reads",
|
|
232
|
+
100 * \
|
|
233
|
+
res_unmap.to_pandas()['count'].fillna(0).sum() / \
|
|
234
|
+
int(res.select(pl.sum("count")).to_pandas()['count'])
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
sgRNA_table = pd.concat([
|
|
238
|
+
library.to_pandas()[['sgID_A','protospacer_A']].rename(columns={'sgID_A':'sgID','protospacer_A':'protospacer'}),
|
|
239
|
+
library.to_pandas()[['sgID_B', 'protospacer_B']].rename(columns={'sgID_B':'sgID','protospacer_B':'protospacer'})
|
|
240
|
+
]).drop_duplicates(keep='first')
|
|
241
|
+
|
|
242
|
+
res_unmap_remapped_a = res_unmap.join(
|
|
243
|
+
pl.DataFrame(sgRNA_table.rename(
|
|
244
|
+
columns={'protospacer':'protospacer_A','sgID':'sgID_A'})[['sgID_A','protospacer_A']]),
|
|
245
|
+
on=["protospacer_A"], how="left"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
res_recomb_events = res_unmap_remapped_a.join(
|
|
249
|
+
pl.DataFrame(sgRNA_table.rename(
|
|
250
|
+
columns={'protospacer':'protospacer_B','sgID':'sgID_B'})[['sgID_B','protospacer_B']]),
|
|
251
|
+
on=["protospacer_B"], how="left"
|
|
252
|
+
)
|
|
253
|
+
if verbose:
|
|
254
|
+
print("% fully remapped recombination events",
|
|
255
|
+
100 * \
|
|
256
|
+
res_recomb_events.drop_nulls().to_pandas()['count'].fillna(0).sum() / \
|
|
257
|
+
int(res.select(pl.sum("count")).to_pandas()['count'])
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if return_type == 'unmapped':
|
|
261
|
+
# TODO: add option to return only unmapped reads after mapping recombinant events
|
|
262
|
+
return res_unmap
|
|
263
|
+
elif return_type == 'mapped':
|
|
264
|
+
return res_map
|
|
265
|
+
elif return_type == 'recombinant':
|
|
266
|
+
if get_recombinant:
|
|
267
|
+
return res_recomb_events
|
|
268
|
+
else:
|
|
269
|
+
raise ValueError("get_recombinant must be set to True to calculate recombinant events")
|
|
270
|
+
elif return_type == 'all':
|
|
271
|
+
if get_recombinant:
|
|
272
|
+
return {'full': res,'mapped': res_map,'recombinant': res_recomb_events, 'unmapped': res_unmap}
|
|
273
|
+
else:
|
|
274
|
+
return {'full': res,'mapped': res_map, 'unmapped': res_unmap}
|
|
275
|
+
else:
|
|
276
|
+
raise ValueError("return_type must be either 'unmapped', 'mapped', 'recombinant', or 'all'")
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
"""phenoscore module
|
|
7
|
+
|
|
8
|
+
This module contains functions for calculating relative phenotypes from CRISPR screens
|
|
9
|
+
datasets.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import anndata as ad
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
from .delta import (
|
|
17
|
+
compareByReplicates, compareByTargetGroup,
|
|
18
|
+
getPhenotypeData,
|
|
19
|
+
calculateDelta,
|
|
20
|
+
getBestTargetByTSS,
|
|
21
|
+
generatePseudoGeneAnnData
|
|
22
|
+
)
|
|
23
|
+
from .deseq import runDESeq, extractDESeqResults
|
|
24
|
+
from ._annotate import annotateScoreTable
|
|
25
|
+
from .phenostat import matrixStat, multipleTestsCorrection
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def runPhenoScore(adata, cond_ref, cond_test, score_level,
|
|
29
|
+
var_names='target', collapse_var=False,
|
|
30
|
+
test='ttest', growth_rate=1, n_reps='auto', keep_top_n = None,
|
|
31
|
+
num_pseudogenes='auto', pseudogene_size='auto',
|
|
32
|
+
count_layer=None, count_filter_type='mean', count_filter_threshold=40,
|
|
33
|
+
ctrl_label='negative_control'
|
|
34
|
+
):
|
|
35
|
+
"""Calculate phenotype score and p-values when comparing `cond_test` vs `cond_ref`.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
adata (AnnData): AnnData object
|
|
39
|
+
cond_ref (str): condition reference
|
|
40
|
+
cond_test (str): condition test
|
|
41
|
+
score_level (str): score level
|
|
42
|
+
var_names (str): variable names to use as index in the result dataframe
|
|
43
|
+
collapse_var (str): variable to use for `getBestTargetByTSS` function, default is False
|
|
44
|
+
test (str): test to use for calculating p-value ('MW': Mann-Whitney U rank; 'ttest' : t-test)
|
|
45
|
+
growth_rate (int): growth rate
|
|
46
|
+
n_reps (int): number of replicates
|
|
47
|
+
keep_top_n (int): number of top guides to keep per target
|
|
48
|
+
num_pseudogenes (int): number of pseudogenes to generate
|
|
49
|
+
pseudogene_size (int): number of sgRNA elements in each pseudogene
|
|
50
|
+
count_layer (str): count layer to use for calculating score, default is None (use default count layer in adata.X)
|
|
51
|
+
count_filter_type (str): filter type for counts, default is 'mean'
|
|
52
|
+
count_filter_threshold (int): filter threshold for counts, default is 40
|
|
53
|
+
ctrl_label (str): control label, default is 'negative_control'
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
str: result name
|
|
57
|
+
pd.DataFrame: result dataframe
|
|
58
|
+
"""
|
|
59
|
+
adat = adata.copy()
|
|
60
|
+
|
|
61
|
+
if 'condition' not in adat.obs.columns:
|
|
62
|
+
raise ValueError("The AnnData object must have a 'condition' column in its obs.")
|
|
63
|
+
|
|
64
|
+
# format result name
|
|
65
|
+
result_name = f'{cond_test}_vs_{cond_ref}'
|
|
66
|
+
print(f'\t{cond_test} vs {cond_ref}')
|
|
67
|
+
|
|
68
|
+
# set n_reps if not provided
|
|
69
|
+
if n_reps == 'auto':
|
|
70
|
+
n_reps = min(
|
|
71
|
+
adat.obs.query(f'condition=="{cond_ref}"').shape[0],
|
|
72
|
+
adat.obs.query(f'condition=="{cond_test}"').shape[0]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# check if count_layer exists
|
|
76
|
+
if count_layer is None:
|
|
77
|
+
pass
|
|
78
|
+
elif count_layer not in adat.layers.keys():
|
|
79
|
+
raise ValueError(f"Layer '{count_layer}' not found in adata.layers.keys().")
|
|
80
|
+
elif count_layer in adat.layers.keys():
|
|
81
|
+
adat.X = adat.layers[count_layer].copy()
|
|
82
|
+
|
|
83
|
+
# calc phenotype score and p-value
|
|
84
|
+
if score_level in ['compare_reps']:
|
|
85
|
+
|
|
86
|
+
# prep counts for phenoScore calculation
|
|
87
|
+
df_cond_ref = adat[adat.obs.query(f'condition=="{cond_ref}"').index[:n_reps],].to_df(count_layer).T
|
|
88
|
+
df_cond_test = adat[adat.obs.query(f'condition=="{cond_test}"').index[:n_reps],].to_df(count_layer).T
|
|
89
|
+
|
|
90
|
+
result = compareByReplicates(
|
|
91
|
+
adata=adat,
|
|
92
|
+
df_cond_ref=df_cond_ref,
|
|
93
|
+
df_cond_test=df_cond_test,
|
|
94
|
+
var_names=var_names,
|
|
95
|
+
test=test,
|
|
96
|
+
ctrl_label=ctrl_label,
|
|
97
|
+
growth_rate=growth_rate,
|
|
98
|
+
filter_type=count_filter_type,
|
|
99
|
+
filter_threshold=count_filter_threshold
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
elif score_level in ['compare_guides']:
|
|
103
|
+
|
|
104
|
+
# prep counts for phenoScore calculation
|
|
105
|
+
df_cond_ref = adat[adat.obs.query(f'condition=="{cond_ref}"').index].to_df().T
|
|
106
|
+
df_cond_test = adat[adat.obs.query(f'condition=="{cond_test}"').index].to_df().T
|
|
107
|
+
del df_cond_ref, df_cond_test
|
|
108
|
+
|
|
109
|
+
adat_pseudo = generatePseudoGeneAnnData(adat, num_pseudogenes=num_pseudogenes, pseudogene_size=pseudogene_size, ctrl_label=ctrl_label)
|
|
110
|
+
if 'transcript' in var_names: adat_pseudo.var['transcript'] = 'na'
|
|
111
|
+
|
|
112
|
+
adat_test = ad.concat([adat[:,~adat.var.targetType.eq(ctrl_label)], adat_pseudo], axis=1)
|
|
113
|
+
adat_test.obs = adat.obs.copy()
|
|
114
|
+
|
|
115
|
+
# prep counts for phenoScore calculation
|
|
116
|
+
df_cond_ref = adat_test[adat_test.obs.query(f'condition=="{cond_ref}"').index].to_df().T
|
|
117
|
+
df_cond_test = adat_test[adat_test.obs.query(f'condition=="{cond_test}"').index].to_df().T
|
|
118
|
+
|
|
119
|
+
result = compareByTargetGroup(
|
|
120
|
+
adata=adat_test,
|
|
121
|
+
df_cond_ref=df_cond_ref,
|
|
122
|
+
df_cond_test=df_cond_test,
|
|
123
|
+
keep_top_n=keep_top_n,
|
|
124
|
+
var_names=var_names,
|
|
125
|
+
test=test,
|
|
126
|
+
ctrl_label=ctrl_label,
|
|
127
|
+
growth_rate=growth_rate,
|
|
128
|
+
filter_type=count_filter_type,
|
|
129
|
+
filter_threshold=count_filter_threshold
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# get the best transcript as lowest p-value for each target
|
|
133
|
+
if collapse_var not in [False, None]:
|
|
134
|
+
if collapse_var not in result.columns:
|
|
135
|
+
raise ValueError(f'collapse_var "{collapse_var}" not found in result columns.')
|
|
136
|
+
else:
|
|
137
|
+
result = getBestTargetByTSS(
|
|
138
|
+
score_df=result, target_col=collapse_var, pvalue_col=f'{test} pvalue'
|
|
139
|
+
)
|
|
140
|
+
result.index.name = None
|
|
141
|
+
|
|
142
|
+
# change target name to control label if it is a pseudo gene
|
|
143
|
+
result['target'] = result['target'].apply(lambda x: ctrl_label if 'pseudo' in x else x).to_list()
|
|
144
|
+
|
|
145
|
+
else:
|
|
146
|
+
raise ValueError(f'score_level "{score_level}" not recognized. Currently, "compare_reps" and "compare_guides" are supported.')
|
|
147
|
+
|
|
148
|
+
return result_name, result
|