Oncodrive3D 1.0.4__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,749 @@
1
+ """
2
+ Contains functions to perform the 3D clustering of missense mutations.
3
+ """
4
+
5
+ import multiprocessing
6
+ import os
7
+ import json
8
+ import daiquiri
9
+ import networkx.algorithms.community as nx_comm
10
+ import numpy as np
11
+ import pandas as pd
12
+
13
+ from scripts import __logger_name__
14
+
15
+ from scripts.run.communities import get_community_index_nx, get_network
16
+ from scripts.run.pvalues import get_final_gene_result
17
+ from scripts.run.miss_mut_prob import get_miss_mut_prob_dict, mut_rate_vec_to_dict, get_unif_gene_miss_prob
18
+ from scripts.run.score_and_simulations import get_anomaly_score, get_sim_anomaly_score, recompute_inf_score
19
+ from scripts.run.utils import add_info, get_gene_entry, add_nan_clust_cols, parse_maf_input, sort_cols, empty_result_pos
20
+ from scripts.run.mutability import init_mutabilities_module
21
+
22
+ logger = daiquiri.getLogger(__logger_name__ + ".run.clustering")
23
+
24
+
25
+ def process_mapping_issue(issue_ix,
26
+ mut_gene_df,
27
+ result_gene_df,
28
+ gene, uniprot_id,
29
+ af_f,
30
+ transcript_status,
31
+ thr,
32
+ issue_type):
33
+ """
34
+ Check if there are mutations not in the structure (mut pos exceed lenght of
35
+ the structure protein sequence) or mutations with mismatches between WT AA
36
+ between mut and structure protein sequence. If the ratio of mutations do
37
+ not exceed threshold, filter out the specific mutations, else filter out
38
+ all mutations of that gene.
39
+ """
40
+
41
+ if issue_type == "Mut_not_in_structure":
42
+ logger_txt="mut not in the structure"
43
+ df_col = "Ratio_not_in_structure"
44
+ elif issue_type == "WT_mismatch":
45
+ logger_txt="mut with ref-structure WT AA mismatch"
46
+ df_col = "Ratio_WT_mismatch"
47
+ else:
48
+ logger.warning(f"'{issue_type}' is not a valid issue type, please select 'Mut_not_in_structure' or 'Ratio_not_in_structure': Skipping processing mapping issue..")
49
+ filter_gene = False
50
+
51
+ return filter_gene, result_gene_df, mut_gene_df
52
+
53
+ ratio_issue = sum(issue_ix) / len(mut_gene_df)
54
+ logger_out = f"Detected {sum(issue_ix)} ({ratio_issue*100:.1f}%) {logger_txt} of {gene} ({uniprot_id}-F{af_f}, transcript status = {transcript_status}): "
55
+ result_gene_df[df_col] = np.round(ratio_issue, 3)
56
+
57
+
58
+ # Do not filter neither gene neither mut
59
+ if issue_type == "WT_mismatch" and thr == 1:
60
+ logger.warning(logger_out + "Filtering of mismatching mutations disabled ('thr_mapping_issue' = 1)..")
61
+ mut_gene_df.loc[issue_ix, "WT_mismatch"] = 1
62
+ filter_gene = False
63
+ return filter_gene, result_gene_df, mut_gene_df
64
+
65
+ else:
66
+ # Filter gene
67
+ if ratio_issue >= thr:
68
+ result_gene_df["Status"] = issue_type
69
+
70
+ if transcript_status == "Match":
71
+ logger.warning(logger_out + "Filtering the gene")
72
+ else:
73
+ logger.debug(logger_out + "Filtering the gene..")
74
+ filter_gene = True
75
+ return filter_gene, result_gene_df, None
76
+
77
+ # Filter mut
78
+ else:
79
+ if transcript_status == "Match":
80
+ logger.warning(logger_out + "Filtering the mutations..")
81
+ else:
82
+ logger.debug(logger_out + "Filtering the mutations..")
83
+ mut_gene_df = mut_gene_df[~issue_ix]
84
+ filter_gene = False
85
+ return filter_gene, result_gene_df, mut_gene_df
86
+
87
+
88
+
89
+ def clustering_3d(gene,
90
+ uniprot_id,
91
+ mut_gene_df,
92
+ cmap_path,
93
+ miss_prob_dict,
94
+ seq_gene,
95
+ af_f,
96
+ alpha=0.01,
97
+ num_iteration=10000,
98
+ cmap_prob_thr=0.5,
99
+ seed=None,
100
+ pae_path=None,
101
+ thr_mapping_issue=0.1,
102
+ sample_info=False):
103
+ """
104
+ Compute local density of missense mutations for a sphere of 10A around
105
+ each amino acid position of the selected gene product. It performed a
106
+ rank-based comparison between observed density and simulated ones in
107
+ absense of positive selection (cohort mut profile). Get an experimental
108
+ per-residue p-val for the local enrichment and a global p-val for the gene,
109
+ which correspond to the minimum p-val across its positions.
110
+
111
+ Parameters: ## CHANGE/UPDATE
112
+ -----------
113
+ gene : str
114
+ mut_gene_df : pandas dataframe
115
+ It must include the mutated positions of the gene.
116
+
117
+ gene_to_uniprot_dict : dict
118
+ Uniprot_ID as key and corresponding genes as values
119
+
120
+ neighbours_df : pandas df
121
+ miss_prob_dict : dict
122
+ Uniprot_ID as keys and per residue prob of missense mut as values
123
+
124
+ af_structures_path : str
125
+ num_iteration : int
126
+ v : bolean
127
+ plot_contact_map : bolean
128
+
129
+ Returns: ## CHANGE/UPDATE
130
+ ----------
131
+ evaluation_df : pandas df (per-position evaluation)
132
+ test_result : pandas df (per-gene summary evaluation)
133
+ status_df : pandas df (per-gene processing status)
134
+ """
135
+
136
+ ## Initialize
137
+
138
+ mut_count = len(mut_gene_df)
139
+ transcript_id_input = mut_gene_df.Transcript_ID.iloc[0]
140
+ transcript_id_o3d = mut_gene_df.O3D_transcript_ID.iloc[0]
141
+ transcript_status = mut_gene_df.Transcript_status.iloc[0]
142
+ result_gene_df = pd.DataFrame({"Gene" : gene,
143
+ "Uniprot_ID" : uniprot_id,
144
+ "F" : af_f,
145
+ "Mut_in_gene" : mut_count,
146
+ "Ratio_not_in_structure" : 0,
147
+ "Ratio_WT_mismatch" : 0,
148
+ "Mut_zero_mut_prob" : 0,
149
+ "Pos_zero_mut_prob" : np.nan,
150
+ "Transcript_ID" : transcript_id_input,
151
+ "O3D_transcript_ID" : transcript_id_o3d,
152
+ "Transcript_status" : transcript_status,
153
+ "Status" : np.nan},
154
+ index=[1])
155
+
156
+
157
+ # Check if there is a mutation that is not in the structure
158
+ if max(mut_gene_df.Pos) > len(seq_gene):
159
+ not_in_structure_ix = mut_gene_df.Pos > len(seq_gene)
160
+ filter_gene, result_gene_df, mut_gene_df = process_mapping_issue(not_in_structure_ix,
161
+ mut_gene_df,
162
+ result_gene_df,
163
+ gene,
164
+ uniprot_id,
165
+ af_f,
166
+ transcript_status,
167
+ thr_mapping_issue,
168
+ issue_type="Mut_not_in_structure")
169
+ if filter_gene:
170
+ return None, result_gene_df
171
+
172
+ # Check for mismatch between WT reference and WT structure
173
+ wt_mismatch_ix = mut_gene_df.apply(lambda x: bool(seq_gene[x.Pos-1] != x.WT), axis=1)
174
+ if sum(wt_mismatch_ix) > 0:
175
+ filter_gene, result_gene_df, mut_gene_df = process_mapping_issue(wt_mismatch_ix,
176
+ mut_gene_df,
177
+ result_gene_df,
178
+ gene,
179
+ uniprot_id,
180
+ af_f,
181
+ transcript_status,
182
+ thr_mapping_issue,
183
+ issue_type="WT_mismatch")
184
+ if filter_gene:
185
+ return None, result_gene_df
186
+
187
+ # Load cmap
188
+ cmap_complete_path = f"{cmap_path}/{uniprot_id}-F{af_f}.npy"
189
+ if os.path.isfile(cmap_complete_path):
190
+ cmap = np.load(cmap_complete_path)
191
+ cmap = cmap > cmap_prob_thr
192
+ cmap = cmap.astype(int)
193
+ else:
194
+ result_gene_df["Status"] = "Cmap_not_found"
195
+ return None, result_gene_df
196
+
197
+ # Load PAE
198
+ pae_complete_path = f"{pae_path}/{uniprot_id}-F{af_f}-predicted_aligned_error.npy"
199
+ if os.path.isfile(pae_complete_path):
200
+ pae = np.load(pae_complete_path)
201
+ else:
202
+ pae = None
203
+
204
+
205
+ ## Get expected local myssense mutation density
206
+
207
+ # Probability that each residue can be hit by a missense mut
208
+ if miss_prob_dict is not None:
209
+ gene_miss_prob = np.array(miss_prob_dict[f"{uniprot_id}-F{af_f}"])
210
+ else:
211
+ gene_miss_prob = get_unif_gene_miss_prob(size=len(cmap))
212
+
213
+ # Filter out genes whose missense prob vec include any NA
214
+ if np.any(np.isnan(gene_miss_prob)):
215
+ result_gene_df["Status"] = "NA_miss_prob"
216
+ return None, result_gene_df
217
+
218
+ # Filter out genes with a mutation in a residue having zero prob to mutate
219
+ pos_vec = np.unique(mut_gene_df["Pos"].values)
220
+ pos_prob_vec = np.array(gene_miss_prob)[pos_vec-1]
221
+ if (pos_prob_vec == 0).any():
222
+ result_gene_df["Status"] = "Mut_with_zero_prob"
223
+ pos_prob_vec = np.array(gene_miss_prob)[pos_vec-1]
224
+ pos_zero_prob = list(pos_vec[pos_prob_vec == 0])
225
+ mut_zero_prob_ix = mut_gene_df["Pos"].isin(pos_zero_prob)
226
+ mut_zero_prob_count = sum(mut_zero_prob_ix)
227
+ result_gene_df["Mut_zero_mut_prob"] = mut_zero_prob_count
228
+ result_gene_df["Pos_zero_mut_prob"] = str(pos_zero_prob)
229
+ ratio_zero_prob = mut_zero_prob_count / len(mut_gene_df)
230
+ logger_out = f"Detected {mut_zero_prob_count} ({ratio_zero_prob*100:.1f}%) mut in {len(pos_zero_prob)} pos {pos_zero_prob} with zero mut prob in {gene} ({uniprot_id}-F{af_f}, transcript status = {transcript_status}): "
231
+ result_gene_df["Ratio_mut_zero_prob"] = ratio_zero_prob
232
+
233
+ if ratio_zero_prob > thr_mapping_issue:
234
+ result_gene_df["Status"] = "Mut_with_zero_prob"
235
+ if transcript_status == "Match":
236
+ logger.warning(logger_out + "Filtering the gene..")
237
+ else:
238
+ logger.debug(logger_out + "Filtering the gene..")
239
+ return None, result_gene_df
240
+ else:
241
+ if transcript_status == "Match":
242
+ logger.warning(logger_out + "Filtering the mutations..")
243
+ else:
244
+ logger.debug(logger_out + "Filtering the mutations..")
245
+ mut_gene_df = mut_gene_df[~mut_zero_prob_ix]
246
+
247
+ # Probability that the volume of each residue can be hit by a missense mut
248
+ vol_missense_mut_prob = np.dot(cmap, gene_miss_prob)
249
+
250
+
251
+ ## Get observed and ranked simulated scores (loglik+_LFC)
252
+
253
+ # Get the observed mut count and densities
254
+ count = mut_gene_df.Pos.value_counts()
255
+ mut_count_v = np.zeros(len(cmap))
256
+ mut_count_v[count.index - 1] = count.values
257
+ mut_count_m = mut_count_v.reshape((1, -1))
258
+ density_m = np.einsum('ij,jk->ki', cmap, mut_count_m.T, optimize=True)
259
+ mutated_pos = np.sort(count.index)
260
+
261
+ # Do not process if there isn't any density larger than 1
262
+ if max(density_m[0][mutated_pos-1]) <= 1:
263
+ result_gene_df["Status"] = "No_density"
264
+ return None, result_gene_df
265
+
266
+ # Inialize result df
267
+ result_pos_df = pd.DataFrame({"Pos" : mutated_pos, "Mut_in_vol" : density_m[0, mutated_pos-1].astype(int)})
268
+
269
+ # Get the ranked simulated score
270
+ sim_anomaly = get_sim_anomaly_score(len(mut_gene_df),
271
+ cmap,
272
+ gene_miss_prob,
273
+ vol_missense_mut_prob,
274
+ num_iteration=num_iteration,
275
+ seed=seed)
276
+
277
+ # Get ranked observed score (loglik+_LFC)
278
+ no_mut_pos = len(result_pos_df)
279
+ sim_anomaly = sim_anomaly.iloc[:no_mut_pos,:].reset_index()
280
+
281
+ result_pos_df["Score"] = get_anomaly_score(result_pos_df["Mut_in_vol"],
282
+ len(mut_gene_df),
283
+ vol_missense_mut_prob[result_pos_df["Pos"]-1])
284
+ if np.isinf(result_pos_df.Score).any():
285
+ logger.debug(f"Detected inf observed score in gene {gene} ({uniprot_id}-F{af_f}): Recomputing with higher precision..")
286
+ result_pos_df = recompute_inf_score(result_pos_df, len(mut_gene_df), vol_missense_mut_prob[result_pos_df["Pos"]-1])
287
+
288
+ mut_in_res = count.rename("Mut_in_res").reset_index().rename(columns={"index" : "Pos"})
289
+ result_pos_df = mut_in_res.merge(result_pos_df, on = "Pos", how = "outer")
290
+ result_pos_df = result_pos_df.sort_values("Score", ascending=False).reset_index(drop=True)
291
+
292
+
293
+ ## Compute p-val and assign hits
294
+
295
+ # Add to the simulated score of each iteration its standard deviation
296
+ # (makes the method more conservative, eg., avoid borderline cases)
297
+ sim_anomaly.iloc[:,1:] = sim_anomaly.apply(lambda x: x[1:] + x[1:].std(), axis=1)
298
+
299
+ # Ratio observed and simulated anomaly scores
300
+ # (used to break the tie in p-values gene sorting)
301
+ result_pos_df["Score_obs_sim"] = sim_anomaly.apply(lambda x: result_pos_df["Score"].values[int(x["index"])] / np.mean(x[1:]), axis=1)
302
+
303
+ # Empirical p-val
304
+ result_pos_df["pval"] = sim_anomaly.apply(lambda x: sum(x[1:] >= result_pos_df["Score"].values[int(x["index"])]) / len(x[1:]), axis=1)
305
+
306
+ # Assign hits
307
+ result_pos_df["C"] = [int(i) for i in result_pos_df["pval"] < alpha]
308
+
309
+ # Select extended significant hits
310
+ pos_hits = result_pos_df[result_pos_df["C"] == 1].Pos
311
+ neigh_pos_hits = list(set([pos for p in pos_hits.values for pos in list(np.where(cmap[p - 1])[0] + 1)]))
312
+ pos_hits_extended = [pos for pos in result_pos_df.Pos if pos in neigh_pos_hits]
313
+ result_pos_df["C_ext"] = result_pos_df.apply(lambda x: 1 if (x["C"] == 0) & (x["Pos"] in pos_hits_extended)
314
+ else 0 if (x["C"] == 1) else np.nan, axis=1)
315
+ result_pos_df["C"] = result_pos_df.apply(lambda x: 1 if (x["C"] == 1) | (x["C_ext"] == 1) else 0, axis=1)
316
+ pos_hits = result_pos_df[result_pos_df["C"] == 1].Pos
317
+
318
+
319
+ ## Communities detection
320
+ if len(pos_hits) > 0:
321
+ if len(pos_hits) > 1:
322
+ # Build network and perform detection
323
+ G = get_network(pos_hits, mut_count_v, cmap)
324
+ communities = nx_comm.label_propagation_communities(G)
325
+ clumps = get_community_index_nx(pos_hits, communities)
326
+
327
+ else:
328
+ # Assign cluster 0 to the only pos hit
329
+ clumps = 0
330
+ meta_clusters = pd.DataFrame({"Pos" : pos_hits, "Clump" : clumps})
331
+ result_pos_df = result_pos_df.merge(meta_clusters, how = "left", on = "Pos")
332
+ else:
333
+ result_pos_df["Clump"] = np.nan
334
+
335
+
336
+ ## Output
337
+ if len(pos_hits) > 0:
338
+ clustered_mut = sum([pos in np.unique(np.concatenate([np.where(cmap[pos-1])[0]+1 for pos in pos_hits.values]))
339
+ for pos in mut_gene_df.Pos])
340
+ else:
341
+ clustered_mut = 0
342
+ result_pos_df["Rank"] = result_pos_df.index
343
+ result_pos_df.insert(0, "Gene", gene)
344
+ result_pos_df.insert(1, "Uniprot_ID", uniprot_id)
345
+ result_pos_df.insert(2, "F", af_f)
346
+ result_pos_df.insert(4, "Mut_in_gene", len(mut_gene_df))
347
+ result_pos_df = add_info(mut_gene_df, result_pos_df, cmap, pae, sample_info)
348
+ result_gene_df["Clust_res"] = len(pos_hits)
349
+ result_gene_df["Clust_mut"] = clustered_mut
350
+ result_gene_df["Status"] = "Processed"
351
+
352
+ return result_pos_df, result_gene_df
353
+
354
+
355
+ def clustering_3d_mp(genes,
356
+ data,
357
+ cmap_path,
358
+ miss_prob_dict,
359
+ seq_df,
360
+ plddt_df,
361
+ num_process,
362
+ alpha=0.01,
363
+ num_iteration=10000,
364
+ cmap_prob_thr=0.5,
365
+ seed=None,
366
+ pae_path=None,
367
+ thr_mapping_issue=0.1,
368
+ sample_info=False):
369
+ """
370
+ Run the 3D-clustering algorithm in parallel on multiple genes.
371
+ """
372
+
373
+ result_gene_lst = []
374
+ result_pos_lst = []
375
+
376
+ for n, gene in enumerate(genes):
377
+
378
+ mut_gene_df = data[data["Gene"] == gene]
379
+ seq_df_gene = seq_df[seq_df["Gene"] == gene]
380
+ uniprot_id = seq_df_gene['Uniprot_ID'].values[0]
381
+ seq = seq_df_gene['Seq'].values[0]
382
+ af_f = seq_df_gene['F'].values[0]
383
+
384
+ # Add confidence to mut_gene_df
385
+ plddt_df_gene_df = plddt_df[plddt_df["Uniprot_ID"] == uniprot_id].drop(columns=["Uniprot_ID"])
386
+ mut_gene_df = mut_gene_df.merge(plddt_df_gene_df, on = ["Pos"], how = "left")
387
+
388
+ pos_result, result_gene = clustering_3d(gene,
389
+ uniprot_id,
390
+ mut_gene_df,
391
+ cmap_path,
392
+ miss_prob_dict,
393
+ seq_gene=seq,
394
+ af_f=af_f,
395
+ alpha=alpha,
396
+ num_iteration=num_iteration,
397
+ cmap_prob_thr=cmap_prob_thr,
398
+ seed=seed,
399
+ pae_path=pae_path,
400
+ thr_mapping_issue=thr_mapping_issue,
401
+ sample_info=sample_info)
402
+ result_gene_lst.append(result_gene)
403
+ if pos_result is not None:
404
+ result_pos_lst.append(pos_result)
405
+
406
+ # Monitor processing
407
+ if n == 0:
408
+ logger.debug(f"Process [{num_process+1}] starting..")
409
+ elif n % 10 == 0:
410
+ logger.debug(f"Process [{num_process+1}] completed [{n+1}/{len(genes)}] structures..")
411
+ elif n+1 == len(genes):
412
+ logger.debug(f"Process [{num_process+1}] completed!")
413
+
414
+ return result_gene_lst, result_pos_lst
415
+
416
+
417
+ def clustering_3d_mp_wrapper(genes,
418
+ data,
419
+ cmap_path,
420
+ miss_prob_dict,
421
+ seq_df,
422
+ plddt_df,
423
+ num_cores,
424
+ alpha=0.01,
425
+ num_iteration=10000,
426
+ cmap_prob_thr=0.5,
427
+ seed=None,
428
+ pae_path=None,
429
+ thr_mapping_issue=0.1,
430
+ sample_info=False):
431
+ """
432
+ Wrapper function to run the 3D-clustering algorithm in parallel on multiple genes.
433
+ """
434
+
435
+ # Split the genes into chunks for each process
436
+ chunk_size = int(len(genes) / num_cores) + 1
437
+ chunks = [genes[i : i + chunk_size] for i in range(0, len(genes), chunk_size)]
438
+ # num_cores = min(num_cores, len(chunks))
439
+
440
+ # Create a pool of processes and run clustering in parallel
441
+ with multiprocessing.Pool(processes = num_cores) as pool:
442
+
443
+ logger.debug(f'Starting [{len(chunks)}] processes..')
444
+ results = pool.starmap(clustering_3d_mp, [(chunk,
445
+ data[data["Gene"].isin(chunk)],
446
+ cmap_path,
447
+ miss_prob_dict,
448
+ seq_df[seq_df["Gene"].isin(chunk)],
449
+ plddt_df[plddt_df["Uniprot_ID"].isin(seq_df.loc[seq_df["Gene"].isin(chunk), "Uniprot_ID"])],
450
+ n_process,
451
+ alpha,
452
+ num_iteration,
453
+ cmap_prob_thr,
454
+ seed,
455
+ pae_path,
456
+ thr_mapping_issue,
457
+ sample_info)
458
+ for n_process, chunk in enumerate(chunks)])
459
+
460
+ # Parse output
461
+ result_pos_lst = [pd.concat(r[1]) for r in results if len(r[1]) > 0]
462
+ if len(result_pos_lst) > 0:
463
+ result_pos = pd.concat(result_pos_lst)
464
+ else:
465
+ result_pos = None
466
+ result_gene = pd.concat([pd.concat(r[0]) for r in results])
467
+
468
+ return result_pos, result_gene
469
+
470
+
471
+ def run_clustering(input_path,
472
+ mut_profile_path,
473
+ mutability_config_path,
474
+ output_dir,
475
+ cmap_path,
476
+ seq_df_path,
477
+ plddt_path,
478
+ pae_path,
479
+ n_iterations,
480
+ alpha,
481
+ cmap_prob_thr,
482
+ cores,
483
+ seed,
484
+ verbose,
485
+ cancer_type,
486
+ cohort,
487
+ no_fragments,
488
+ only_processed,
489
+ thr_mapping_issue,
490
+ o3d_transcripts,
491
+ use_input_symbols,
492
+ mane,
493
+ sample_info):
494
+ """
495
+ Main function to lunch the 3D clustering analysis.
496
+ """
497
+
498
+ # Load
499
+ # ====
500
+
501
+ seq_df = pd.read_csv(seq_df_path, sep="\t")
502
+ data, seq_df = parse_maf_input(input_path,
503
+ seq_df,
504
+ use_o3d_transcripts=o3d_transcripts,
505
+ use_input_symbols=use_input_symbols,
506
+ mane=mane)
507
+
508
+ if len(data) > 0:
509
+
510
+ # Run
511
+ # ===
512
+
513
+ # Get genes with enough mut
514
+ result_np_gene_lst = []
515
+ genes = data.groupby("Gene").apply(len)
516
+ genes_mut = genes[genes >= 2]
517
+ genes_no_mut = genes[genes < 2].index
518
+
519
+ if len(genes_no_mut) > 0:
520
+ logger.debug(f"Detected [{len(genes_no_mut)}] genes without enough mutations: Skipping..")
521
+ result_gene = pd.DataFrame({"Gene" : genes_no_mut,
522
+ "Uniprot_ID" : np.nan,
523
+ "F" : np.nan,
524
+ "Mut_in_gene" : 1,
525
+ "Ratio_not_in_structure" : np.nan,
526
+ "Ratio_WT_mismatch" : np.nan,
527
+ "Mut_zero_mut_prob" : np.nan,
528
+ "Pos_zero_mut_prob" : np.nan,
529
+ "Transcript_ID" : get_gene_entry(data, genes_no_mut, "Transcript_ID"),
530
+ "O3D_transcript_ID" : get_gene_entry(data, genes_no_mut, "O3D_transcript_ID"),
531
+ "Transcript_status" : get_gene_entry(data, genes_no_mut, "Transcript_status"),
532
+ "Status" : "No_mut"})
533
+ result_np_gene_lst.append(result_gene)
534
+
535
+ # Seq df for metadata info
536
+ metadata_cols = [col for col in ["Gene", "HGNC_ID", "Ens_Gene_ID", "Ens_Transcr_ID", "Refseq_prot", "Uniprot_ID", "F"] if col in seq_df.columns]
537
+ metadata_mapping_cols = [col for col in ["Seq", "Chr", "Reverse_strand", "Exons_coord", "Seq_dna", "Tri_context", "Reference_info"] if col in seq_df.columns]
538
+ seq_df_all = seq_df[seq_df["Gene"].isin(genes.index)].copy()
539
+
540
+ # Get genes with corresponding Uniprot-ID mapping
541
+ gene_to_uniprot_dict = {gene : uni_id for gene, uni_id in seq_df[["Gene", "Uniprot_ID"]].drop_duplicates().values}
542
+ genes_to_process = [gene for gene in genes_mut.index if gene in gene_to_uniprot_dict.keys()]
543
+ seq_df = seq_df[seq_df["Gene"].isin(genes_to_process)].reset_index(drop=True)
544
+ genes_no_mapping = genes[[gene in genes_mut.index and gene not in gene_to_uniprot_dict.keys() for gene in genes.index]]
545
+ if len(genes_no_mapping) > 0:
546
+ logger.debug(f"Detected [{len(genes_no_mapping)}] genes without IDs mapping: Skipping..")
547
+ result_gene = pd.DataFrame({"Gene" : genes_no_mapping.index,
548
+ "Uniprot_ID" : np.nan,
549
+ "F" : np.nan,
550
+ "Mut_in_gene" : genes_no_mapping.values,
551
+ "Ratio_not_in_structure" : np.nan,
552
+ "Ratio_WT_mismatch" : np.nan,
553
+ "Mut_zero_mut_prob" : np.nan,
554
+ "Pos_zero_mut_prob" : np.nan,
555
+ "Transcript_ID" : get_gene_entry(data, genes_no_mapping.index, "Transcript_ID"),
556
+ "O3D_transcript_ID" : get_gene_entry(data, genes_no_mapping.index, "O3D_transcript_ID"),
557
+ "Transcript_status" : get_gene_entry(data, genes_no_mapping.index, "Transcript_status"),
558
+ "Status" : "No_ID_mapping"})
559
+ result_np_gene_lst.append(result_gene)
560
+
561
+ # Filter on fragmented (AF-F) genes
562
+ if no_fragments:
563
+ # Return the fragmented genes as non processed output
564
+ genes_frag = seq_df[seq_df.F.str.extract(r'(\d+)', expand=False).astype(int) > 1]
565
+ genes_frag = genes_frag.Gene.reset_index(drop=True).values
566
+ genes_frag_mut = genes_mut[[gene in genes_frag for gene in genes_mut.index]]
567
+ genes_frag = genes_frag_mut.index.values
568
+ if len(genes_frag) > 0:
569
+ logger.debug(f"Detected [{len(genes_frag)}] fragmented genes with disabled fragments processing: Skipping..")
570
+ result_gene = pd.DataFrame({"Gene" : genes_frag,
571
+ "Uniprot_ID" : np.nan,
572
+ "F" : np.nan,
573
+ "Mut_in_gene" : genes_frag_mut.values,
574
+ "Ratio_not_in_structure" : np.nan,
575
+ "Ratio_WT_mismatch" : np.nan,
576
+ "Mut_zero_mut_prob" : np.nan,
577
+ "Pos_zero_mut_prob" : np.nan,
578
+ "Transcript_ID" : get_gene_entry(data, genes_frag, "Transcript_ID"),
579
+ "O3D_transcript_ID" : get_gene_entry(data, genes_frag, "O3D_transcript_ID"),
580
+ "Transcript_status" : get_gene_entry(data, genes_frag, "Transcript_status"),
581
+ "Status" : "Fragmented"})
582
+ result_np_gene_lst.append(result_gene)
583
+ # Filter out from genes to process and seq df
584
+ genes_to_process = [gene for gene in genes_to_process if gene not in genes_frag]
585
+ seq_df = seq_df[seq_df["Gene"].isin(genes_to_process)].reset_index(drop=True)
586
+
587
+ # Filter on start-loss mutations
588
+ start_mut_ix = data["Pos"] == 1
589
+ start_mut = sum(start_mut_ix)
590
+ if start_mut > 0:
591
+ genes_start_mut = list(data[start_mut_ix].Gene.unique())
592
+ data = data[~start_mut_ix]
593
+ logger.warning(f"Detected {start_mut} start-loss mutations in {len(genes_start_mut)} genes {genes_start_mut}: Filtering mutations..")
594
+
595
+
596
+ # Missense mut prob
597
+ # =================
598
+
599
+ # Using mutabilities if provided
600
+ if mutability_config_path is not None:
601
+ logger.info("Computing missense mut probabilities using mutabilities..")
602
+ mutab_config = json.load(open(mutability_config_path, encoding="utf-8"))
603
+ logger.debug("Init mutabilities module..")
604
+ init_mutabilities_module(mutab_config)
605
+ seq_df = seq_df[seq_df["Reference_info"] == 1]
606
+ seq_df['Exons_coord'] = seq_df['Exons_coord'].apply(eval)
607
+ genes_to_process = [gene for gene in genes_to_process if gene in seq_df["Gene"].unique()]
608
+ genes_not_mutability = [gene for gene in genes_to_process if gene not in seq_df["Gene"].unique()]
609
+ logger.debug("Computing probabilities..")
610
+ miss_prob_dict = get_miss_mut_prob_dict(mut_rate_dict=None, seq_df=seq_df,
611
+ mutability=True, mutability_config=mutab_config)
612
+
613
+ if len(genes_not_mutability) > 0:
614
+ logger.debug(f"Detected [{len(genes_not_mutability)}] genes without mutability information: Skipping..")
615
+ result_gene = pd.DataFrame({"Gene" : genes_not_mutability,
616
+ "Uniprot_ID" : np.nan,
617
+ "F" : np.nan,
618
+ "Mut_in_gene" : np.nan,
619
+ "Ratio_not_in_structure" : np.nan,
620
+ "Ratio_WT_mismatch" : np.nan,
621
+ "Mut_zero_mut_prob" : np.nan,
622
+ "Pos_zero_mut_prob" : np.nan,
623
+ "Transcript_ID" : get_gene_entry(data, genes_not_mutability, "Transcript_ID"),
624
+ "O3D_transcript_ID" : get_gene_entry(data, genes_not_mutability, "O3D_transcript_ID"),
625
+ "Transcript_status" : get_gene_entry(data, genes_not_mutability, "Transcript_status"),
626
+ "Status" : "No_mutability"})
627
+ result_np_gene_lst.append(result_gene)
628
+
629
+ # Using mutational profiles
630
+ elif mut_profile_path is not None:
631
+ # Compute dict from mut profile of the cohort and dna sequences
632
+ mut_profile = json.load(open(mut_profile_path, encoding="utf-8"))
633
+ logger.info("Computing missense mut probabilities..")
634
+ if not isinstance(mut_profile, dict):
635
+ mut_profile = mut_rate_vec_to_dict(mut_profile)
636
+ miss_prob_dict = get_miss_mut_prob_dict(mut_rate_dict=mut_profile, seq_df=seq_df)
637
+ else:
638
+ logger.warning("Mutation profile not provided: Uniform distribution will be used for scoring and simulations.")
639
+ miss_prob_dict = None
640
+
641
+
642
+ # Run 3D-clustering
643
+ # =================
644
+
645
+ if len(result_np_gene_lst):
646
+ result_np_gene = pd.concat(result_np_gene_lst)
647
+ result_np_gene["Uniprot_ID"] = [gene_to_uniprot_dict[gene] if gene in gene_to_uniprot_dict.keys() else np.nan for gene in result_np_gene["Gene"].values]
648
+ if len(genes_to_process) > 0:
649
+ logger.info(f"Performing 3D-clustering on [{len(seq_df)}] proteins..")
650
+ seq_df = seq_df[["Gene", "Uniprot_ID", "F", "Seq"]]
651
+ plddt_df = pd.read_csv(plddt_path, sep="\t", usecols=["Pos", "Confidence", "Uniprot_ID"], dtype={"Pos" : np.int32,
652
+ "Confidence" : np.float32,
653
+ "Uniprot_ID" : "object"})
654
+
655
+ result_pos, result_gene = clustering_3d_mp_wrapper(genes=genes_to_process,
656
+ data=data,
657
+ cmap_path=cmap_path,
658
+ miss_prob_dict=miss_prob_dict,
659
+ seq_df=seq_df,
660
+ plddt_df=plddt_df,
661
+ num_cores=cores,
662
+ alpha=alpha,
663
+ num_iteration=n_iterations,
664
+ cmap_prob_thr=cmap_prob_thr,
665
+ seed=seed,
666
+ pae_path=pae_path,
667
+ thr_mapping_issue=thr_mapping_issue,
668
+ sample_info=sample_info)
669
+ if result_np_gene_lst:
670
+ result_gene = pd.concat((result_gene, result_np_gene))
671
+ else:
672
+ result_gene = result_np_gene
673
+ result_pos = None
674
+
675
+
676
+ # Save
677
+ #=====
678
+
679
+ os.makedirs(output_dir, exist_ok=True)
680
+ result_gene["Cancer"] = cancer_type
681
+ result_gene["Cohort"] = cohort
682
+ output_path_pos = os.path.join(output_dir, f"{cohort}.3d_clustering_pos.csv")
683
+ output_path_genes = os.path.join(output_dir, f"{cohort}.3d_clustering_genes.csv")
684
+
685
+ # Save processed seq_df and input files
686
+ seq_df_output = os.path.join(output_dir, f"{cohort}.seq_df.processed.tsv")
687
+ input_mut_output = os.path.join(output_dir, f"{cohort}.mutations.processed.tsv")
688
+ input_prob_output = os.path.join(output_dir, f"{cohort}.miss_prob.processed.json")
689
+ logger.info(f"Saving {seq_df_output}")
690
+ seq_df_all[metadata_cols + metadata_mapping_cols].to_csv(seq_df_output, sep="\t", index=False)
691
+ logger.info(f"Saving {input_mut_output}")
692
+ data.to_csv(input_mut_output, sep="\t", index=False)
693
+ logger.info(f"Saving {input_prob_output}")
694
+ with open(input_prob_output, "w") as json_file:
695
+ json.dump(miss_prob_dict, json_file)
696
+
697
+ # Add extra metadata
698
+ result_gene = result_gene.drop(columns=["F"]).merge(seq_df_all[metadata_cols], on=["Gene", "Uniprot_ID"], how="left")
699
+
700
+ if only_processed:
701
+ result_gene = result_gene[result_gene["Status"] == "Processed"]
702
+
703
+ if result_pos is None:
704
+ # Save gene-level result and empty res-level result
705
+ logger.warning("Did not processed any genes!")
706
+ result_gene = add_nan_clust_cols(result_gene)
707
+ result_gene = sort_cols(result_gene)
708
+ if not sample_info:
709
+ result_gene.drop(columns=[col for col in ['Tot_samples',
710
+ 'Samples_in_top_vol',
711
+ 'Samples_in_top_cl_vol'] if col in result_gene.columns], inplace=True)
712
+ if no_fragments:
713
+ result_gene = result_gene.drop(columns=[col for col in ["Mut_in_top_F", "Top_F"] if col in result_gene.columns])
714
+ empty_result_pos(sample_info).to_csv(output_path_pos, index=False)
715
+ result_gene.to_csv(output_path_genes, index=False)
716
+
717
+ logger.info(f"Saving (empty) {output_path_pos}")
718
+ logger.info(f"Saving {output_path_genes}")
719
+
720
+ else:
721
+ # Save res-level result
722
+ result_pos["Cancer"] = cancer_type
723
+ result_pos["Cohort"] = cohort
724
+ if not sample_info:
725
+ result_pos.drop(columns=[col for col in ['Tot_samples',
726
+ 'Samples_in_vol',
727
+ 'Samples_in_cl_vol'] if col in result_gene.columns], inplace=True)
728
+ result_pos = result_pos.sort_values(["Gene", "pval", "Score_obs_sim"], ascending=[True, True, False]).reset_index(drop=True)
729
+ result_pos.drop(columns=["F"], errors="ignore").to_csv(output_path_pos, index=False)
730
+
731
+ # Get gene global pval, qval, and clustering annotations and save gene-level result
732
+ result_gene = get_final_gene_result(result_pos, result_gene, alpha, sample_info)
733
+ result_gene = sort_cols(result_gene)
734
+ if not sample_info:
735
+ result_gene.drop(columns=[col for col in ['Tot_samples',
736
+ 'Samples_in_top_vol',
737
+ 'Samples_in_top_cl_vol'] if col in result_gene.columns], inplace=True)
738
+ if no_fragments:
739
+ result_gene.drop(columns=[col for col in ["Mut_in_top_F", "Top_F"] if col in result_gene.columns], inplace=True)
740
+ with np.printoptions(linewidth=10000):
741
+ result_gene.to_csv(output_path_genes, index=False)
742
+
743
+ logger.info(f"Saving {output_path_pos}")
744
+ logger.info(f"Saving {output_path_genes}")
745
+
746
+ logger.info("3D-clustering analysis completed!")
747
+
748
+ else:
749
+ logger.warning("No missense mutations were found in the input MAF. Consider checking your data: the field 'Variant_Classification' should include either 'Missense_Mutation' or 'missense_variant'")