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.
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