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.
- oncodrive3d-1.0.4.dist-info/METADATA +333 -0
- oncodrive3d-1.0.4.dist-info/RECORD +36 -0
- oncodrive3d-1.0.4.dist-info/WHEEL +4 -0
- oncodrive3d-1.0.4.dist-info/entry_points.txt +5 -0
- oncodrive3d-1.0.4.dist-info/licenses/LICENSE +15 -0
- scripts/__init__.py +2 -0
- scripts/clustering_3d.code-workspace +7 -0
- scripts/datasets/__init__.py +0 -0
- scripts/datasets/af_merge.py +344 -0
- scripts/datasets/build_datasets.py +125 -0
- scripts/datasets/get_pae.py +78 -0
- scripts/datasets/get_structures.py +107 -0
- scripts/datasets/model_confidence.py +97 -0
- scripts/datasets/parse_pae.py +64 -0
- scripts/datasets/prob_contact_maps.py +258 -0
- scripts/datasets/seq_for_mut_prob.py +900 -0
- scripts/datasets/utils.py +394 -0
- scripts/globals.py +169 -0
- scripts/main.py +650 -0
- scripts/plotting/__init__.py +0 -0
- scripts/plotting/build_annotations.py +102 -0
- scripts/plotting/chimerax_plot.py +251 -0
- scripts/plotting/pdb_tool.py +149 -0
- scripts/plotting/pfam.py +94 -0
- scripts/plotting/plot.py +2484 -0
- scripts/plotting/stability_change.py +184 -0
- scripts/plotting/uniprot_feat.py +276 -0
- scripts/plotting/utils.py +594 -0
- scripts/run/__init__.py +0 -0
- scripts/run/clustering.py +749 -0
- scripts/run/communities.py +53 -0
- scripts/run/miss_mut_prob.py +206 -0
- scripts/run/mutability.py +289 -0
- scripts/run/pvalues.py +91 -0
- scripts/run/score_and_simulations.py +155 -0
- scripts/run/utils.py +461 -0
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module to generate a pandas dataframe including identifiers mapped to protein and DNA sequences.
|
|
3
|
+
|
|
4
|
+
The functions are used to extract all protein sequences of PDB structures in a given directory;
|
|
5
|
+
use EMBOSS backtranseq back translate proteins sequences into DNA; generate a dataframe including HUGO symbol,
|
|
6
|
+
Uniprot_ID, protein, and DNA sequences. This dataframe is required to get the probability of each residue to mutate
|
|
7
|
+
(missense mutation) based on the mutation profile (mutation rate in 96 trinucleotide contexts) of the cohort.
|
|
8
|
+
The per-residue missense mutation probability of each protein is then used to get the probability of a
|
|
9
|
+
certain volume to be hit by a missense mutation.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import json
|
|
15
|
+
import multiprocessing
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import shlex
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
import daiquiri
|
|
24
|
+
import numpy as np
|
|
25
|
+
import pandas as pd
|
|
26
|
+
import requests
|
|
27
|
+
from bgreference import hg38, mm39
|
|
28
|
+
from Bio.Seq import Seq
|
|
29
|
+
from tqdm import tqdm
|
|
30
|
+
|
|
31
|
+
from scripts import __logger_name__
|
|
32
|
+
from scripts.datasets.utils import (
|
|
33
|
+
download_single_file,
|
|
34
|
+
get_af_id_from_pdb,
|
|
35
|
+
get_pdb_path_list_from_dir,
|
|
36
|
+
get_seq_from_pdb,
|
|
37
|
+
uniprot_to_hugo,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
logger = daiquiri.getLogger(__logger_name__ + ".build.seq_for_mut_prob")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#===========
|
|
44
|
+
# region Initialize
|
|
45
|
+
#===========
|
|
46
|
+
|
|
47
|
+
def initialize_seq_df(input_path, uniprot_to_gene_dict):
|
|
48
|
+
"""
|
|
49
|
+
Parse any PDB structure from a given directory and create
|
|
50
|
+
a dataframe including HUGO symbol, Uniprot-ID, AF fragment,
|
|
51
|
+
and protein sequence.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
list_prot_path = get_pdb_path_list_from_dir(input_path)
|
|
55
|
+
gene_lst = []
|
|
56
|
+
uni_id_lst = []
|
|
57
|
+
f_lst = []
|
|
58
|
+
seq_lst = []
|
|
59
|
+
|
|
60
|
+
for path_structure in tqdm(list_prot_path, total=len(list_prot_path), desc="Generating sequence df"):
|
|
61
|
+
uniprot_id, f = get_af_id_from_pdb(path_structure).split("-F")
|
|
62
|
+
if uniprot_id in uniprot_to_gene_dict:
|
|
63
|
+
gene = uniprot_to_gene_dict[uniprot_id]
|
|
64
|
+
else:
|
|
65
|
+
gene = np.nan
|
|
66
|
+
seq = "".join(list(get_seq_from_pdb(path_structure)))
|
|
67
|
+
gene_lst.append(gene)
|
|
68
|
+
uni_id_lst.append(uniprot_id)
|
|
69
|
+
f_lst.append(f)
|
|
70
|
+
seq_lst.append(seq)
|
|
71
|
+
|
|
72
|
+
seq_df = pd.DataFrame({"Gene" : gene_lst,
|
|
73
|
+
"Uniprot_ID" : uni_id_lst,
|
|
74
|
+
"F" : f_lst,
|
|
75
|
+
"Seq" : seq_lst
|
|
76
|
+
}).sort_values(["Gene", "F"])
|
|
77
|
+
|
|
78
|
+
return seq_df
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
#==============================================
|
|
82
|
+
# Get DNA sequence using EMBL backtranseq API
|
|
83
|
+
# (not 100% reliable but available fo most seq)
|
|
84
|
+
#==============================================
|
|
85
|
+
|
|
86
|
+
def backtranseq(protein_seqs, organism = "Homo sapiens"):
|
|
87
|
+
"""
|
|
88
|
+
Perform backtranslation from proteins to DNA sequences using EMBOS backtranseq.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
# Define the API endpoints
|
|
92
|
+
run_url = "https://www.ebi.ac.uk/Tools/services/rest/emboss_backtranseq/run"
|
|
93
|
+
status_url = "https://www.ebi.ac.uk/Tools/services/rest/emboss_backtranseq/status/"
|
|
94
|
+
result_url = "https://www.ebi.ac.uk/Tools/services/rest/emboss_backtranseq/result/"
|
|
95
|
+
|
|
96
|
+
# Define the parameters for the API request (an email address must be included)
|
|
97
|
+
params = {"email": "stefano.pellegrini@irbbarcelona.org",
|
|
98
|
+
"sequence": protein_seqs,
|
|
99
|
+
"outseqformat": "plain",
|
|
100
|
+
"molecule": "dna",
|
|
101
|
+
"organism": organism}
|
|
102
|
+
|
|
103
|
+
# Submit the job request and retrieve the job ID
|
|
104
|
+
response = "INIT"
|
|
105
|
+
while str(response) != "<Response [200]>":
|
|
106
|
+
if response != "INIT":
|
|
107
|
+
time.sleep(10)
|
|
108
|
+
try:
|
|
109
|
+
response = requests.post(run_url, data=params, timeout=160)
|
|
110
|
+
except requests.exceptions.RequestException as e:
|
|
111
|
+
response = "ERROR"
|
|
112
|
+
logger.debug(f"Request failed: {e}")
|
|
113
|
+
|
|
114
|
+
job_id = response.text.strip()
|
|
115
|
+
|
|
116
|
+
# Wait for the job to complete
|
|
117
|
+
status = "INIT"
|
|
118
|
+
while status != "FINISHED":
|
|
119
|
+
time.sleep(20)
|
|
120
|
+
try:
|
|
121
|
+
result = requests.get(status_url + job_id, timeout=160)
|
|
122
|
+
status = result.text.strip()
|
|
123
|
+
except requests.exceptions.RequestException as e:
|
|
124
|
+
status = "ERROR"
|
|
125
|
+
logger.debug(f"Request failed {e}: Retrying..")
|
|
126
|
+
|
|
127
|
+
# Retrieve the results of the job
|
|
128
|
+
status = "INIT"
|
|
129
|
+
while status != "FINISHED":
|
|
130
|
+
try:
|
|
131
|
+
result = requests.get(result_url + job_id + "/out", timeout=160)
|
|
132
|
+
status = "FINISHED"
|
|
133
|
+
except requests.exceptions.RequestException as e:
|
|
134
|
+
status = "ERROR"
|
|
135
|
+
logger.debug(f"Request failed {e}: Retrying..")
|
|
136
|
+
time.sleep(10)
|
|
137
|
+
|
|
138
|
+
dna_seq = result.text.strip()
|
|
139
|
+
|
|
140
|
+
return dna_seq
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def batch_backtranseq(df, batch_size, organism = "Homo sapiens"):
|
|
144
|
+
"""
|
|
145
|
+
Given a dataframe including protein sequences, it divides the
|
|
146
|
+
sequences into batches of a given size and run EMBOSS backtranseq
|
|
147
|
+
(https://www.ebi.ac.uk/Tools/st/emboss_backtranseq/) to translate
|
|
148
|
+
them into DNA sequences.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
batches = df.groupby(df.index // batch_size)
|
|
152
|
+
lst_batches = []
|
|
153
|
+
|
|
154
|
+
# Iterate over batches
|
|
155
|
+
for i, batch in tqdm(batches, total=len(batches), desc="Backtranseq"):
|
|
156
|
+
|
|
157
|
+
# Avoid sending too many request
|
|
158
|
+
if i+1 == 30:
|
|
159
|
+
logger.debug("Reached maximum number of requests: waiting 180s..")
|
|
160
|
+
time.sleep(180)
|
|
161
|
+
|
|
162
|
+
# Get input format for backtranseq
|
|
163
|
+
batch_seq = "\n".join(batch.reset_index(drop=True).apply(lambda x: f'>\n{x["Seq"]}', axis=1).values)
|
|
164
|
+
|
|
165
|
+
# Run backtranseq
|
|
166
|
+
batch_dna = backtranseq(batch_seq, organism = organism)
|
|
167
|
+
|
|
168
|
+
# Parse output
|
|
169
|
+
batch_dna = re.split(">EMBOSS_\d+", batch_dna.replace("\n", ""))[1:]
|
|
170
|
+
|
|
171
|
+
batch["Seq_dna"] = batch_dna
|
|
172
|
+
lst_batches.append(batch)
|
|
173
|
+
|
|
174
|
+
return pd.concat(lst_batches)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
#===============================================================
|
|
178
|
+
# Get exons (CDS) coordinate using EMBL Proteins Coordinates API
|
|
179
|
+
#===============================================================
|
|
180
|
+
|
|
181
|
+
def _uniprot_request_coord(lst_uniprot_ids):
|
|
182
|
+
"""
|
|
183
|
+
Use Coordinates from EMBL-EBI Proteins API to get
|
|
184
|
+
a json including exons coordinate and protein info.
|
|
185
|
+
|
|
186
|
+
https://www.ebi.ac.uk/proteins/api/doc/#coordinatesApi
|
|
187
|
+
https://doi.org/10.1093/nar/gkx237
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
prot_request = [f"{prot}" if i == 0 else f"%2C{prot}" for i, prot in enumerate(lst_uniprot_ids)]
|
|
191
|
+
requestURL = f"https://www.ebi.ac.uk/proteins/api/coordinates?offset=0&size=100&accession={''.join(prot_request)}"
|
|
192
|
+
|
|
193
|
+
status = "INIT"
|
|
194
|
+
while status != "FINISHED":
|
|
195
|
+
if status != "INIT":
|
|
196
|
+
time.sleep(10)
|
|
197
|
+
try:
|
|
198
|
+
r = requests.get(requestURL, headers={ "Accept" : "application/json"}, timeout=160)
|
|
199
|
+
if r.ok:
|
|
200
|
+
status = "FINISHED"
|
|
201
|
+
else:
|
|
202
|
+
logger.debug(f"Error occurred after successfully sending request {r.raise_for_status()}: Retrying..")
|
|
203
|
+
status = "ERROR"
|
|
204
|
+
except requests.exceptions.RequestException as e:
|
|
205
|
+
status = "ERROR"
|
|
206
|
+
logger.debug(f"Request failed ({e}): Retrying..")
|
|
207
|
+
|
|
208
|
+
for dictio in json.loads(r.text):
|
|
209
|
+
|
|
210
|
+
yield dictio
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_sorted_transcript_lst(dictio, canonical_ids):
|
|
214
|
+
"""
|
|
215
|
+
Sort a list of tuple (index, transcript ID) placing the
|
|
216
|
+
Ensembl canonical transcript as first element, if present.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
lst_tuple_ix_id = [(i, coord_dict["ensemblTranscriptId"]) for (i, coord_dict) in enumerate(dictio)]
|
|
220
|
+
|
|
221
|
+
return sorted(lst_tuple_ix_id, key=lambda x: x[1] in canonical_ids, reverse=True)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_batch_exons_coord(batch_ids, ens_canonical_transcripts_lst):
|
|
225
|
+
"""
|
|
226
|
+
Parse the json obtained from the Coordinates service extracting
|
|
227
|
+
exons coordinates and protein info.
|
|
228
|
+
|
|
229
|
+
https://www.ebi.ac.uk/proteins/api/doc/#coordinatesApi
|
|
230
|
+
https://doi.org/10.1093/nar/gkx237
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
lst_uni_id = []
|
|
234
|
+
lst_ens_gene_id = []
|
|
235
|
+
lst_ens_transcr_id = []
|
|
236
|
+
lst_seq = []
|
|
237
|
+
lst_chr = []
|
|
238
|
+
lst_reverse = []
|
|
239
|
+
lst_ranges = []
|
|
240
|
+
|
|
241
|
+
for dic in _uniprot_request_coord(batch_ids):
|
|
242
|
+
|
|
243
|
+
uni_id = dic["accession"]
|
|
244
|
+
seq = dic["sequence"]
|
|
245
|
+
|
|
246
|
+
# Iterate throug the transcripts (starting from Ensembl canonical one if present)
|
|
247
|
+
sorted_transcript_lst = get_sorted_transcript_lst(dic["gnCoordinate"], ens_canonical_transcripts_lst)
|
|
248
|
+
for i, ens_transcr_id in sorted_transcript_lst:
|
|
249
|
+
|
|
250
|
+
ens_gene_id = dic["gnCoordinate"][i]["ensemblGeneId"]
|
|
251
|
+
dic_loc = dic["gnCoordinate"][i]["genomicLocation"]
|
|
252
|
+
exons = dic_loc["exon"]
|
|
253
|
+
chromosome = dic_loc["chromosome"]
|
|
254
|
+
reverse_strand = dic_loc["reverseStrand"]
|
|
255
|
+
ranges = []
|
|
256
|
+
|
|
257
|
+
for exon in exons:
|
|
258
|
+
exon = exon["genomeLocation"]
|
|
259
|
+
if "begin" in exon.keys():
|
|
260
|
+
begin = exon["begin"]["position"]
|
|
261
|
+
end = exon["end"]["position"]
|
|
262
|
+
else:
|
|
263
|
+
begin = exon["position"]["position"]
|
|
264
|
+
end = exon["position"]["position"]
|
|
265
|
+
ranges.append((begin, end))
|
|
266
|
+
|
|
267
|
+
# Check if the DNA seq of the transcript is equal the codon lenght
|
|
268
|
+
start = 0 if not int(reverse_strand) else 1
|
|
269
|
+
end = 1 if not int(reverse_strand) else 0
|
|
270
|
+
len_dna = sum([coord[end] - coord[start] + 1 for coord in ranges])
|
|
271
|
+
if len_dna / 3 == len(seq):
|
|
272
|
+
break
|
|
273
|
+
# If there is no transcript with matching sequence length, return NA
|
|
274
|
+
elif i == len(dic["gnCoordinate"]) - 1:
|
|
275
|
+
ens_gene_id = np.nan
|
|
276
|
+
ens_transcr_id = np.nan
|
|
277
|
+
chromosome = np.nan
|
|
278
|
+
reverse_strand = np.nan
|
|
279
|
+
ranges = np.nan
|
|
280
|
+
|
|
281
|
+
lst_uni_id.append(uni_id)
|
|
282
|
+
lst_ens_gene_id.append(ens_gene_id)
|
|
283
|
+
lst_ens_transcr_id.append(ens_transcr_id)
|
|
284
|
+
lst_seq.append(seq)
|
|
285
|
+
lst_chr.append(chromosome)
|
|
286
|
+
reverse_strand = int(reverse_strand) if isinstance(reverse_strand, int) else np.nan
|
|
287
|
+
ranges = str(ranges) if isinstance(ranges, list) else np.nan
|
|
288
|
+
lst_reverse.append(reverse_strand)
|
|
289
|
+
lst_ranges.append(ranges)
|
|
290
|
+
|
|
291
|
+
return pd.DataFrame({"Uniprot_ID" : lst_uni_id,
|
|
292
|
+
"Ens_Gene_ID" : lst_ens_gene_id,
|
|
293
|
+
"Ens_Transcr_ID" : lst_ens_transcr_id,
|
|
294
|
+
"Seq" : lst_seq,
|
|
295
|
+
"Chr" : lst_chr,
|
|
296
|
+
"Reverse_strand" : lst_reverse,
|
|
297
|
+
"Exons_coord" : lst_ranges})
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def get_exons_coord(ids, ens_canonical_transcripts_lst, batch_size=100):
|
|
301
|
+
"""
|
|
302
|
+
Use the Coordinates service from Proteins API of EMBL-EBI to get
|
|
303
|
+
exons coordinate and proteins info of all provided Uniprot IDs.
|
|
304
|
+
|
|
305
|
+
https://www.ebi.ac.uk/proteins/api/doc/#coordinatesApi
|
|
306
|
+
https://doi.org/10.1093/nar/gkx237
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
lst_df = []
|
|
310
|
+
batches_ids = [ids[i:i+batch_size] for i in range(0, len(ids), batch_size)]
|
|
311
|
+
|
|
312
|
+
for batch_ids in tqdm(batches_ids, total=len(batches_ids), desc="Adding exons coordinate"):
|
|
313
|
+
|
|
314
|
+
batch_df = get_batch_exons_coord(batch_ids, ens_canonical_transcripts_lst)
|
|
315
|
+
|
|
316
|
+
# Identify unmapped IDs and add them as NaN rows
|
|
317
|
+
unmapped_ids = list(set(batch_ids).difference(set(batch_df.Uniprot_ID.unique())))
|
|
318
|
+
nan = np.repeat(np.nan, len(unmapped_ids))
|
|
319
|
+
nan_rows = pd.DataFrame({'Uniprot_ID' : unmapped_ids,
|
|
320
|
+
'Ens_Gene_ID' : nan,
|
|
321
|
+
'Ens_Transcr_ID' : nan,
|
|
322
|
+
'Seq': nan,
|
|
323
|
+
'Chr': nan,
|
|
324
|
+
'Reverse_strand' : nan,
|
|
325
|
+
'Exons_coord': nan})
|
|
326
|
+
|
|
327
|
+
batch_df = pd.concat([batch_df, nan_rows], ignore_index=True)
|
|
328
|
+
lst_df.append(batch_df)
|
|
329
|
+
|
|
330
|
+
return pd.concat(lst_df).reset_index(drop=True)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
#=======================================================================
|
|
334
|
+
# Get DNA sequence and trin-context of reference genome from coordinates
|
|
335
|
+
#=======================================================================
|
|
336
|
+
|
|
337
|
+
def per_site_trinucleotide_context(seq, no_flanks=False):
|
|
338
|
+
"""
|
|
339
|
+
Given a DNA sequence rapresenting an exon (CDS) with -1 and +1 flanking
|
|
340
|
+
sites, return a list including the trinucletide context of each site.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
# If there is no info about flanking regions, add most the two most common ones
|
|
344
|
+
if no_flanks:
|
|
345
|
+
seq = f"C{seq}T"
|
|
346
|
+
|
|
347
|
+
return [f"{seq[i-1]}{seq[i]}{seq[i+1]}" for i in range(1, len(seq) - 1)]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def get_ref_dna_and_context(row, genome_fun):
|
|
351
|
+
"""
|
|
352
|
+
Given a row of the sequence dataframe including exons (CDS) coordinate,
|
|
353
|
+
get the corresponding DNA sequence and the trinucleotide context of each
|
|
354
|
+
site of the sequence taking into account flaking regions at splicing sites.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
transcript_id = row["Ens_Transcr_ID"]
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
lst_exon_coord = ast.literal_eval(row["Exons_coord"])
|
|
361
|
+
reverse = row["Reverse_strand"]
|
|
362
|
+
chrom = row["Chr"]
|
|
363
|
+
|
|
364
|
+
lst_exon_tri_context = []
|
|
365
|
+
lst_exon_seq = []
|
|
366
|
+
|
|
367
|
+
for region in lst_exon_coord:
|
|
368
|
+
|
|
369
|
+
# Retrieve reference seq of the exon (CDS)
|
|
370
|
+
start = 0 if not reverse else 1
|
|
371
|
+
end = 1 if not reverse else 0
|
|
372
|
+
segment_len = region[end] - region[start] + 1
|
|
373
|
+
seq_with_flanks = genome_fun(chrom, region[start] - 1, size = segment_len + 2)
|
|
374
|
+
|
|
375
|
+
# Get reverse complement if reverse strand
|
|
376
|
+
if reverse:
|
|
377
|
+
seq_with_flanks = str(Seq(seq_with_flanks).reverse_complement())
|
|
378
|
+
|
|
379
|
+
# Get the trinucleotide context of each site of the exon (CDS)
|
|
380
|
+
per_site_tri_context = ",".join(per_site_trinucleotide_context(seq_with_flanks))
|
|
381
|
+
|
|
382
|
+
# Get the reference DNA sequence
|
|
383
|
+
seq = seq_with_flanks[1:-1]
|
|
384
|
+
|
|
385
|
+
lst_exon_tri_context.append(per_site_tri_context)
|
|
386
|
+
lst_exon_seq.append(seq)
|
|
387
|
+
|
|
388
|
+
return transcript_id, "".join(lst_exon_seq), ",".join(lst_exon_tri_context), 1
|
|
389
|
+
|
|
390
|
+
except Exception as e:
|
|
391
|
+
if not str(e).startswith("Sequence"):
|
|
392
|
+
logger.warning(f"Error occurred during retrieving DNA seq from ref coordinates {transcript_id} {e}: Skipping..")
|
|
393
|
+
|
|
394
|
+
return np.nan, np.nan, np.nan, -1
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def add_ref_dna_and_context(seq_df, genome_fun=hg38):
|
|
398
|
+
"""
|
|
399
|
+
Given as input the sequence dataframe including exons (CDS) coordinate,
|
|
400
|
+
add the corresponding DNA sequence and the trinucleotide context of each
|
|
401
|
+
site of the sequence taking into account flaking regions at splicing sites.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
seq_df = seq_df.copy()
|
|
405
|
+
seq_df_with_coord = seq_df[["Ens_Transcr_ID", "Chr", "Reverse_strand", "Exons_coord"]].dropna().drop_duplicates()
|
|
406
|
+
ref_dna_and_context = seq_df_with_coord.apply(lambda x: get_ref_dna_and_context(x, genome_fun), axis=1, result_type='expand')
|
|
407
|
+
ref_dna_and_context.columns = ["Ens_Transcr_ID", "Seq_dna", "Tri_context", "Reference_info"]
|
|
408
|
+
seq_df = seq_df.merge(ref_dna_and_context, on=["Ens_Transcr_ID"], how="left").drop_duplicates().reset_index(drop=True)
|
|
409
|
+
seq_df["Reference_info"] = seq_df["Reference_info"].fillna(-1).astype(int)
|
|
410
|
+
seq_df.loc[seq_df["Reference_info"] == -1, "Ens_Transcr_ID"] = np.nan
|
|
411
|
+
seq_df.loc[seq_df["Reference_info"] == -1, "Seq_dna"] = np.nan
|
|
412
|
+
seq_df.loc[seq_df["Reference_info"] == -1, "Exons_coord"] = np.nan
|
|
413
|
+
seq_df.loc[seq_df["Reference_info"] == -1, "Tri_context"] = np.nan
|
|
414
|
+
|
|
415
|
+
return seq_df
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
#=========
|
|
419
|
+
# WRAPPERS
|
|
420
|
+
#=========
|
|
421
|
+
|
|
422
|
+
def add_extra_genes_to_seq_df(seq_df, uniprot_to_gene_dict):
|
|
423
|
+
"""
|
|
424
|
+
If multiple genes are mapping to a given Uniprot_ID, add
|
|
425
|
+
each gene name with corresponding sequence info to the seq_df.
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
lst_added_genes = []
|
|
429
|
+
lst_extra_genes_rows = []
|
|
430
|
+
for _, seq_row in seq_df.iterrows():
|
|
431
|
+
uni_id = seq_row["Uniprot_ID"]
|
|
432
|
+
if uni_id in uniprot_to_gene_dict:
|
|
433
|
+
gene_id = uniprot_to_gene_dict[uni_id]
|
|
434
|
+
|
|
435
|
+
if not pd.isna(gene_id):
|
|
436
|
+
gene_id = gene_id.split()
|
|
437
|
+
|
|
438
|
+
if len(gene_id) > 1:
|
|
439
|
+
for gene in gene_id:
|
|
440
|
+
if gene != seq_row["Gene"] and gene not in lst_added_genes:
|
|
441
|
+
row = seq_row.copy()
|
|
442
|
+
row["Gene"] = gene
|
|
443
|
+
lst_extra_genes_rows.append(row)
|
|
444
|
+
lst_added_genes.append(gene)
|
|
445
|
+
|
|
446
|
+
seq_df_extra_genes = pd.concat(lst_extra_genes_rows, axis=1).T
|
|
447
|
+
|
|
448
|
+
# Remove rows with multiple symbols and drop duplicated ones
|
|
449
|
+
seq_df = pd.concat((seq_df, seq_df_extra_genes))
|
|
450
|
+
seq_df = seq_df.dropna(subset=["Gene"])
|
|
451
|
+
seq_df = seq_df[seq_df.apply(lambda x: len(x["Gene"].split()), axis =1) == 1].reset_index(drop=True)
|
|
452
|
+
seq_df = seq_df.drop_duplicates().reset_index(drop=True)
|
|
453
|
+
|
|
454
|
+
return seq_df
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def download_mane_summary(path_to_file, v=1.3, max_attempts=15):
|
|
458
|
+
"""
|
|
459
|
+
Download the summary.txt of the MANE release from NCBI.
|
|
460
|
+
"""
|
|
461
|
+
|
|
462
|
+
mane_summary_url = f"https://ftp.ncbi.nlm.nih.gov/refseq/MANE/MANE_human/release_{v}/MANE.GRCh38.v{v}.summary.txt.gz"
|
|
463
|
+
attempts = 0
|
|
464
|
+
|
|
465
|
+
while not os.path.exists(path_to_file):
|
|
466
|
+
download_single_file(mane_summary_url, path_to_file, threads=1)
|
|
467
|
+
attempts += 1
|
|
468
|
+
if attempts >= max_attempts:
|
|
469
|
+
raise RuntimeError(f"Failed to download MANE summary file after {max_attempts} attempts. Exiting..")
|
|
470
|
+
time.sleep(5)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def select_uni_id(ids_tuple, all_ids):
|
|
474
|
+
"""
|
|
475
|
+
Return the first Uniprot ID present in the list of IDs
|
|
476
|
+
(list of structures available). If no ID of the tuple
|
|
477
|
+
maps a downloaded structure, return NA.
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
for uni_id in ids_tuple:
|
|
481
|
+
if uni_id in all_ids:
|
|
482
|
+
return uni_id
|
|
483
|
+
|
|
484
|
+
return np.nan
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def get_mane_to_af_mapping(datasets_dir, downloaded_uniprot_ids, include_not_af=False, mane_version=1.0):
|
|
488
|
+
"""
|
|
489
|
+
Get a dataframe to map genes, MANE transcript IDs, and protein structures.
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
mane_to_af = pd.read_csv(os.path.join(datasets_dir, "mane_refseq_prot_to_alphafold.csv"))
|
|
493
|
+
mane_to_af = mane_to_af.rename(columns={"refseq_prot" : "Refseq_prot",
|
|
494
|
+
"uniprot_accession" : "Uniprot_ID"}).drop(columns=["alphafold"])
|
|
495
|
+
path_mane_summary = os.path.join(datasets_dir, "mane_summary.txt.gz")
|
|
496
|
+
if not os.path.exists(path_mane_summary):
|
|
497
|
+
download_mane_summary(path_mane_summary, mane_version)
|
|
498
|
+
mane = pd.read_csv(path_mane_summary, compression='gzip', sep="\t").dropna(subset=["symbol", "HGNC_ID"])
|
|
499
|
+
mane = mane.rename(columns={"symbol" : "Gene",
|
|
500
|
+
"RefSeq_prot" : "Refseq_prot",
|
|
501
|
+
"Ensembl_Gene" : "Ens_Gene_ID",
|
|
502
|
+
"Ensembl_nuc" : "Ens_Transcr_ID",
|
|
503
|
+
"GRCh38_chr" : "Chr",
|
|
504
|
+
"chr_strand" : "Reverse_strand"})
|
|
505
|
+
mane = mane[["Gene", "Refseq_prot", "Ens_Gene_ID", "Ens_Transcr_ID", "Chr", "Reverse_strand"]]
|
|
506
|
+
mane_mapping = mane_to_af.merge(mane, how="left", on="Refseq_prot").dropna()
|
|
507
|
+
mane_mapping.Reverse_strand = mane_mapping.Reverse_strand.map({"+" : 0, "-" : 1})
|
|
508
|
+
mane_mapping.Ens_Gene_ID = mane_mapping.Ens_Gene_ID.apply(lambda x: x.split(".")[0])
|
|
509
|
+
mane_mapping.Ens_Transcr_ID = mane_mapping.Ens_Transcr_ID.apply(lambda x: x.split(".")[0])
|
|
510
|
+
|
|
511
|
+
# Select first Uniprot ID if multiple ones are present
|
|
512
|
+
mane_mapping["Uniprot_ID"] = mane_mapping.apply(lambda x:
|
|
513
|
+
select_uni_id(x.Uniprot_ID.split(";"), downloaded_uniprot_ids) if len(x.Uniprot_ID.split(";")) > 1
|
|
514
|
+
else x.Uniprot_ID, axis=1)
|
|
515
|
+
mane_mapping = mane_mapping.reset_index(drop=True)
|
|
516
|
+
|
|
517
|
+
if include_not_af:
|
|
518
|
+
mane_not_af = mane[~mane.Gene.isin(mane_mapping.Gene)].reset_index(drop=True)
|
|
519
|
+
|
|
520
|
+
return mane_mapping, mane_not_af
|
|
521
|
+
|
|
522
|
+
else:
|
|
523
|
+
return mane_mapping
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def download_biomart_metadata(path_to_file):
|
|
527
|
+
"""
|
|
528
|
+
Query biomart to get the list of transcript corresponding to the downloaded
|
|
529
|
+
structures (a few structures are missing) and other information.
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
# command = f"""
|
|
533
|
+
# wget -O {path_to_file} 'http://jan2024.archive.ensembl.org/biomart/martservice?query=<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Query><Query virtualSchemaName="default" formatter="TSV" header="0" uniqueRows="0" count="" datasetConfigVersion="0.6"><Dataset name="hsapiens_gene_ensembl" interface="default"><Attribute name="ensembl_gene_id"/><Attribute name="ensembl_transcript_id"/><Attribute name="transcript_is_canonical"/><Attribute name="external_gene_name"/><Attribute name="external_gene_source"/><Attribute name="hgnc_id"/><Attribute name="uniprot_gn_id"/><Attribute name="uniprotswissprot"/></Dataset></Query>'
|
|
534
|
+
# """
|
|
535
|
+
|
|
536
|
+
command = f"""
|
|
537
|
+
wget -O {path_to_file} 'http://jan2024.archive.ensembl.org/biomart/martservice?query=<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Query><Query virtualSchemaName="default" formatter="TSV" header="0" uniqueRows="0" count="" datasetConfigVersion="0.6"><Dataset name="hsapiens_gene_ensembl" interface="default"><Attribute name="ensembl_gene_id"/><Attribute name="ensembl_transcript_id"/><Attribute name="transcript_is_canonical"/><Attribute name="external_gene_name"/><Attribute name="external_gene_source"/><Attribute name="hgnc_id"/><Attribute name="uniprot_gn_id"/><Attribute name="uniprotswissprot"/><Attribute name="external_synonym"/></Dataset></Query>'
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
subprocess.run(shlex.split(command))
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def get_biomart_metadata(datasets_dir, uniprot_ids):
|
|
544
|
+
"""
|
|
545
|
+
Download a dataframe including ensembl canonical transcript IDs,
|
|
546
|
+
HGNC IDs, Uniprot IDs, and other useful information.
|
|
547
|
+
"""
|
|
548
|
+
|
|
549
|
+
download_biomart_metadata(os.path.join(datasets_dir, "biomart_metadata.tsv"))
|
|
550
|
+
|
|
551
|
+
# Parse
|
|
552
|
+
biomart_df = pd.read_csv(os.path.join(datasets_dir, "biomart_metadata.tsv"), sep="\t", header=None)
|
|
553
|
+
biomart_df.columns= ["Ens_Gene_ID", "Ens_Transcr_ID", "Ens_Canonical", "Gene", "Gene_source", "HGNC_ID", "Uniprot_ID", "UniprotKB_ID", "Gene_synonym"]
|
|
554
|
+
biomart_df = biomart_df.dropna(subset=["Uniprot_ID", "UniprotKB_ID"], how='all')
|
|
555
|
+
biomart_df = biomart_df[biomart_df["Gene_source"] == "HGNC Symbol"].drop(columns=["Gene_source"])
|
|
556
|
+
biomart_df.reset_index(inplace=True, drop=True)
|
|
557
|
+
|
|
558
|
+
# Filter
|
|
559
|
+
uniprot_ids = pd.Series(uniprot_ids)
|
|
560
|
+
uniprot_kb_ids = uniprot_ids[(uniprot_ids.isin(biomart_df.UniprotKB_ID))]
|
|
561
|
+
uniprot_notkb_ids = uniprot_ids[~(uniprot_ids.isin(biomart_df.UniprotKB_ID)) & (uniprot_ids.isin(biomart_df.Uniprot_ID))]
|
|
562
|
+
biomart_uniprotkb = biomart_df[biomart_df.UniprotKB_ID.isin(uniprot_kb_ids)].drop(
|
|
563
|
+
columns=["Uniprot_ID"]).rename(columns={"UniprotKB_ID" : "Uniprot_ID"}).drop_duplicates()
|
|
564
|
+
biomart_uniprot = biomart_df[biomart_df.Uniprot_ID.isin(uniprot_notkb_ids)].drop(columns=["UniprotKB_ID"]).drop_duplicates()
|
|
565
|
+
biomart_df = pd.concat((biomart_uniprotkb, biomart_uniprot)).reset_index(drop=True)
|
|
566
|
+
|
|
567
|
+
# Output
|
|
568
|
+
# (ATM this is only used to get Ensembl canonical transcript
|
|
569
|
+
# but, in the future, it could be used to add extra info such as
|
|
570
|
+
# HGNC, and any other features we want from biomart)
|
|
571
|
+
biomart_df.to_csv(os.path.join(datasets_dir, "biomart_metadata.tsv"), sep="\t", index=False)
|
|
572
|
+
ens_canonical_transcripts = biomart_df.Ens_Transcr_ID[biomart_df["Ens_Canonical"] == 1].unique()
|
|
573
|
+
# gene_ids = biomart_df[["Gene", "HGNC_ID"]]
|
|
574
|
+
# gene_ids_syn = biomart_df[["Gene_synonym", "HGNC_ID"]].rename(columns={"Gene_synonym" : "Gene"})
|
|
575
|
+
# gene_ids = pd.concat((gene_ids, gene_ids_syn)).sort_values("HGNC_ID").reset_index(drop=True).drop_duplicates()
|
|
576
|
+
|
|
577
|
+
return ens_canonical_transcripts
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def get_ref_dna_from_ensembl(transcript_id):
|
|
581
|
+
"""
|
|
582
|
+
Use Ensembl GET sequence rest API to obtain CDS DNA
|
|
583
|
+
sequence from Ensembl transcript ID.
|
|
584
|
+
|
|
585
|
+
https://rest.ensembl.org/documentation/info/sequence_id
|
|
586
|
+
"""
|
|
587
|
+
|
|
588
|
+
server = "https://rest.ensembl.org"
|
|
589
|
+
ext = f"/sequence/id/{transcript_id}?type=cds"
|
|
590
|
+
|
|
591
|
+
status = "INIT"
|
|
592
|
+
i = 0
|
|
593
|
+
while status != "FINISHED":
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
r = requests.get(server+ext, headers={ "Content-Type" : "text/x-fasta"}, timeout=160)
|
|
597
|
+
if not r.ok:
|
|
598
|
+
r.raise_for_status()
|
|
599
|
+
|
|
600
|
+
status = "ERROR"
|
|
601
|
+
sys.exit()
|
|
602
|
+
else:
|
|
603
|
+
status = "FINISHED"
|
|
604
|
+
|
|
605
|
+
except requests.exceptions.RequestException as e:
|
|
606
|
+
i += 1
|
|
607
|
+
status = "ERROR"
|
|
608
|
+
if i%10 == 0:
|
|
609
|
+
logger.debug(f"Failed to retrieve sequence for {transcript_id} {e}: Retrying..")
|
|
610
|
+
time.sleep(5)
|
|
611
|
+
if i == 100:
|
|
612
|
+
logger.debug(f"Failed to retrieve sequence for {transcript_id} {e}: Skipping..")
|
|
613
|
+
return np.nan
|
|
614
|
+
|
|
615
|
+
time.sleep(1)
|
|
616
|
+
|
|
617
|
+
seq_dna = "".join(r.text.strip().split("\n")[1:])
|
|
618
|
+
|
|
619
|
+
return seq_dna[:len(seq_dna)-3]
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def get_ref_dna_from_ensembl_wrapper(ensembl_id):
|
|
623
|
+
"""
|
|
624
|
+
Wrapper for multiple processing function using
|
|
625
|
+
Ensembl Get sequence rest API.
|
|
626
|
+
"""
|
|
627
|
+
|
|
628
|
+
return get_ref_dna_from_ensembl(ensembl_id)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def get_ref_dna_from_ensembl_mp(seq_df, cores):
|
|
632
|
+
"""
|
|
633
|
+
Multiple processing function to use Ensembl GET sequence
|
|
634
|
+
rest API to obtain CDS DNA sequence from Ensembl transcript ID.
|
|
635
|
+
|
|
636
|
+
https://rest.ensembl.org/documentation/info/sequence_id
|
|
637
|
+
"""
|
|
638
|
+
|
|
639
|
+
pool = multiprocessing.Pool(processes=cores)
|
|
640
|
+
seq_df = seq_df.copy()
|
|
641
|
+
seq_df["Seq_dna"] = pool.map(get_ref_dna_from_ensembl_wrapper, seq_df.Ens_Transcr_ID)
|
|
642
|
+
pool.close()
|
|
643
|
+
pool.join()
|
|
644
|
+
|
|
645
|
+
return seq_df
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def drop_gene_duplicates(df):
|
|
649
|
+
"""
|
|
650
|
+
Issue: multiple Uniprot IDs might map to the same HUGO symbol.
|
|
651
|
+
Drop Uniprot ID of gene duplicates by prioritizing reference
|
|
652
|
+
info status (Proteins API > Ensembl API > Backtranseq API).
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
df = df.copy()
|
|
656
|
+
df.Uniprot_ID = df.Uniprot_ID.str.replace(";", "")
|
|
657
|
+
df.Gene = df.Gene.str.replace(";", "")
|
|
658
|
+
df = df.sort_values(["Gene", "Reference_info"], ascending=False).drop_duplicates(subset='Gene').reset_index(drop=True)
|
|
659
|
+
|
|
660
|
+
# Check if there are still duplicates
|
|
661
|
+
n_duplicates = sum(df["Gene"].value_counts() > 1)
|
|
662
|
+
if n_duplicates > 0:
|
|
663
|
+
logger.warning(f"Found {n_duplicates} duplicates gene entries: Mapping HUGO Symbol to protein info might be affected.")
|
|
664
|
+
else:
|
|
665
|
+
logger.debug("Duplicates gene entries correctly removed!")
|
|
666
|
+
|
|
667
|
+
return df
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def process_seq_df(seq_df,
|
|
671
|
+
datasets_dir,
|
|
672
|
+
organism,
|
|
673
|
+
uniprot_to_gene_dict,
|
|
674
|
+
ens_canonical_transcripts_lst,
|
|
675
|
+
num_cores=1,
|
|
676
|
+
rm_weird_chr=False,
|
|
677
|
+
mane_version=1.3):
|
|
678
|
+
"""
|
|
679
|
+
Retrieve DNA sequence and tri-nucleotide context
|
|
680
|
+
for each structure in the initialized dataframe
|
|
681
|
+
prioritizing structures obtained from transcripts
|
|
682
|
+
whose exon coordinates are available in the Proteins API.
|
|
683
|
+
|
|
684
|
+
Reference_info labels:
|
|
685
|
+
1 : Transcript ID, exons coord, seq DNA obtained from Proteins API
|
|
686
|
+
-1 : Not available transcripts, seq DNA retrieved from Backtranseq API
|
|
687
|
+
"""
|
|
688
|
+
|
|
689
|
+
# Process entries in Proteins API (Reference_info 1)
|
|
690
|
+
#---------------------------------------------------
|
|
691
|
+
|
|
692
|
+
# Add coordinates for mutability integration (entries in Proteins API)
|
|
693
|
+
logger.debug(f"Retrieving CDS DNA seq from reference genome (Proteins API): {len(seq_df['Uniprot_ID'].unique())} structures..")
|
|
694
|
+
coord_df = get_exons_coord(seq_df["Uniprot_ID"].unique(), ens_canonical_transcripts_lst)
|
|
695
|
+
seq_df = seq_df.merge(coord_df, on=["Seq", "Uniprot_ID"], how="left").reset_index(drop=True)
|
|
696
|
+
|
|
697
|
+
# Add ref DNA seq and its per-site trinucleotide context (entries in Proteins API)
|
|
698
|
+
if organism == "Homo sapiens":
|
|
699
|
+
logger.debug("Loading reference genome hg38..")
|
|
700
|
+
genome_fun = hg38
|
|
701
|
+
elif organism == "Mus musculus":
|
|
702
|
+
logger.debug("Loading reference genome mm39..")
|
|
703
|
+
genome_fun = mm39
|
|
704
|
+
else:
|
|
705
|
+
raise RuntimeError(f"Failed to recognize '{organism}' as organism. Currently accepted ones are 'Homo sapiens' and 'Mus musculus'. Exiting..")
|
|
706
|
+
seq_df = add_ref_dna_and_context(seq_df, genome_fun)
|
|
707
|
+
seq_df_uniprot = seq_df[seq_df["Reference_info"] == 1]
|
|
708
|
+
seq_df_not_uniprot = seq_df[seq_df["Reference_info"] == -1]
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
# Process entries not in Proteins API (Reference_info -1)
|
|
712
|
+
#------------------------------------------------------------
|
|
713
|
+
|
|
714
|
+
# Add DNA seq from Backtranseq for any other entry
|
|
715
|
+
logger.debug(f"Retrieving CDS DNA seq for entries without available transcript ID (Backtranseq API): {len(seq_df_not_uniprot)} structures..")
|
|
716
|
+
seq_df_not_uniprot = batch_backtranseq(seq_df_not_uniprot, 500, organism=organism)
|
|
717
|
+
|
|
718
|
+
# Get trinucleotide context
|
|
719
|
+
seq_df_not_uniprot["Tri_context"] = seq_df_not_uniprot["Seq_dna"].apply(
|
|
720
|
+
lambda x: ",".join(per_site_trinucleotide_context(x, no_flanks=True)))
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
# Prepare final output
|
|
724
|
+
#---------------------
|
|
725
|
+
|
|
726
|
+
# Concat the dfs, expand multiple genes associated to the same structure, keep only one structure for each gene
|
|
727
|
+
seq_df = pd.concat((seq_df_uniprot, seq_df_not_uniprot)).reset_index(drop=True)
|
|
728
|
+
logger_report = ", ".join([f"{v}: {c}" for (v, c) in zip(seq_df.Reference_info.value_counts().index,
|
|
729
|
+
seq_df.Reference_info.value_counts().values)])
|
|
730
|
+
logger.info(f"Built of sequence dataframe completed. Retrieved {len(seq_df)} structures ({logger_report})")
|
|
731
|
+
seq_df = add_extra_genes_to_seq_df(seq_df, uniprot_to_gene_dict)
|
|
732
|
+
seq_df = drop_gene_duplicates(seq_df)
|
|
733
|
+
|
|
734
|
+
return seq_df
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def process_seq_df_mane(seq_df,
|
|
738
|
+
datasets_dir,
|
|
739
|
+
uniprot_to_gene_dict,
|
|
740
|
+
ens_canonical_transcripts_lst,
|
|
741
|
+
num_cores=1,
|
|
742
|
+
mane_version=1.3):
|
|
743
|
+
"""
|
|
744
|
+
Retrieve DNA sequence and tri-nucleotide context
|
|
745
|
+
for each structure in the initialized dataframe
|
|
746
|
+
prioritizing MANE associated structures and metadata.
|
|
747
|
+
|
|
748
|
+
Reference_info labels:
|
|
749
|
+
1 : Transcript ID, exons coord, seq DNA obtained from Proteins API
|
|
750
|
+
0 : Transcript ID retrieved from MANE and seq DNA from Ensembl
|
|
751
|
+
-1 : Not available transcripts, seq DNA retrieved from Backtranseq API
|
|
752
|
+
"""
|
|
753
|
+
|
|
754
|
+
mane_mapping, mane_mapping_not_af = get_mane_to_af_mapping(datasets_dir,
|
|
755
|
+
seq_df["Uniprot_ID"].unique(),
|
|
756
|
+
include_not_af=True,
|
|
757
|
+
mane_version=mane_version)
|
|
758
|
+
seq_df_mane = seq_df[seq_df.Uniprot_ID.isin(mane_mapping.Uniprot_ID)].reset_index(drop=True)
|
|
759
|
+
seq_df_nomane = seq_df[~seq_df.Uniprot_ID.isin(mane_mapping.Uniprot_ID)].reset_index(drop=True)
|
|
760
|
+
|
|
761
|
+
# Seq df MANE
|
|
762
|
+
seq_df_mane = seq_df_mane.drop(columns=["Gene"]).merge(mane_mapping, how="left", on="Uniprot_ID")
|
|
763
|
+
seq_df_mane["Reference_info"] = 0
|
|
764
|
+
|
|
765
|
+
# Add DNA seq from Ensembl for structures with available transcript ID
|
|
766
|
+
logger.debug(f"Retrieving CDS DNA seq from transcript ID (Ensembl API): {len(seq_df_mane)} structures..")
|
|
767
|
+
seq_df_mane = get_ref_dna_from_ensembl_mp(seq_df_mane, cores=num_cores)
|
|
768
|
+
|
|
769
|
+
# Set failed and len-mismatching entries as no-transcripts entries
|
|
770
|
+
failed_ix = seq_df_mane.apply(lambda x: True if pd.isna(x.Seq_dna) else len(x.Seq_dna) / 3 != len(x.Seq), axis=1)
|
|
771
|
+
if sum(failed_ix) > 0:
|
|
772
|
+
seq_df_mane_failed = seq_df_mane[failed_ix]
|
|
773
|
+
seq_df_mane = seq_df_mane[~failed_ix]
|
|
774
|
+
seq_df_mane_failed = seq_df_mane_failed.drop(columns=["Ens_Gene_ID", "Ens_Transcr_ID", "Reverse_strand",
|
|
775
|
+
"Chr", "Refseq_prot", "Reference_info", "Seq_dna"])
|
|
776
|
+
seq_df_nomane = pd.concat((seq_df_nomane, seq_df_mane_failed))
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
# Seq df not MANE
|
|
780
|
+
seq_df_nomane = add_extra_genes_to_seq_df(seq_df_nomane, uniprot_to_gene_dict) # Filter out genes with NA
|
|
781
|
+
seq_df_nomane = seq_df_nomane[seq_df_nomane.Gene.isin(mane_mapping_not_af.Gene)] # Filter out genes that are not in MANE list
|
|
782
|
+
|
|
783
|
+
# Retrieve seq from coordinates
|
|
784
|
+
logger.debug(f"Retrieving CDS DNA seq from reference genome (Proteins API): {len(seq_df_nomane['Uniprot_ID'].unique())} structures..")
|
|
785
|
+
coord_df = get_exons_coord(seq_df_nomane["Uniprot_ID"].unique(), ens_canonical_transcripts_lst)
|
|
786
|
+
seq_df_nomane = seq_df_nomane.merge(coord_df, on=["Seq", "Uniprot_ID"], how="left").reset_index(drop=True) # Discard entries whose Seq obtained by Proteins API don't exactly match the one in structure
|
|
787
|
+
seq_df_nomane = add_ref_dna_and_context(seq_df_nomane, hg38)
|
|
788
|
+
seq_df_nomane_tr = seq_df_nomane[seq_df_nomane["Reference_info"] == 1]
|
|
789
|
+
seq_df_nomane_notr = seq_df_nomane[seq_df_nomane["Reference_info"] == -1]
|
|
790
|
+
|
|
791
|
+
# Add DNA seq from Backtranseq for any other entry
|
|
792
|
+
logger.debug(f"Retrieving CDS DNA seq for genes without available transcript ID (Backtranseq API): {len(seq_df_nomane_notr)} structures..")
|
|
793
|
+
seq_df_nomane_notr = batch_backtranseq(seq_df_nomane_notr, 500, organism="Homo sapiens")
|
|
794
|
+
|
|
795
|
+
# Get trinucleotide context
|
|
796
|
+
seq_df_not_uniprot = pd.concat((seq_df_mane, seq_df_nomane_notr))
|
|
797
|
+
seq_df_not_uniprot["Tri_context"] = seq_df_not_uniprot["Seq_dna"].apply(
|
|
798
|
+
lambda x: ",".join(per_site_trinucleotide_context(x, no_flanks=True)))
|
|
799
|
+
|
|
800
|
+
# Prepare final output
|
|
801
|
+
seq_df = pd.concat((seq_df_not_uniprot, seq_df_nomane_tr)).reset_index(drop=True)
|
|
802
|
+
seq_df = drop_gene_duplicates(seq_df)
|
|
803
|
+
report_df = seq_df.Reference_info.value_counts().reset_index()
|
|
804
|
+
report_df = report_df.rename(columns={"index" : "Source"})
|
|
805
|
+
report_df.Source = report_df.Source.map({1 : "Proteins API",
|
|
806
|
+
0 : "MANE + Ensembl API",
|
|
807
|
+
-1 : "Backtranseq API"})
|
|
808
|
+
logger_report = ", ".join([f"{v}: {c}" for (v, c) in zip(report_df.Source,
|
|
809
|
+
report_df.Reference_info)])
|
|
810
|
+
logger.debug(f"Built of sequence dataframe completed. Retrieved {len(seq_df)} structures ({logger_report})")
|
|
811
|
+
|
|
812
|
+
return seq_df
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def get_seq_df(datasets_dir,
|
|
816
|
+
output_seq_df,
|
|
817
|
+
organism = "Homo sapiens",
|
|
818
|
+
mane=False,
|
|
819
|
+
num_cores=1,
|
|
820
|
+
rm_weird_chr=False,
|
|
821
|
+
mane_version=1.3):
|
|
822
|
+
"""
|
|
823
|
+
Generate a dataframe including IDs mapping information, the protein
|
|
824
|
+
sequence, the DNA sequence and its tri-nucleotide context, which is
|
|
825
|
+
used to compute the per-residue probability of missense mutation.
|
|
826
|
+
|
|
827
|
+
The DNA sequence and the tri-nucleotide context can be obtained by the
|
|
828
|
+
Proteins API (obtaining the coordinates of the exons and then using them
|
|
829
|
+
to obtain the sequence from the reference genome), Ensembl GET sequence API
|
|
830
|
+
(from Ensembl transcript ID), and from Backtranseq API for all other entries.
|
|
831
|
+
|
|
832
|
+
https://www.ebi.ac.uk/proteins/api/doc/
|
|
833
|
+
https://rest.ensembl.org/documentation/info/sequence_id
|
|
834
|
+
https://www.ebi.ac.uk/jdispatcher/st/emboss_backtranseq
|
|
835
|
+
"""
|
|
836
|
+
|
|
837
|
+
# region Initialization
|
|
838
|
+
#===============
|
|
839
|
+
|
|
840
|
+
# Load Uniprot ID to HUGO and MANE to AF mapping
|
|
841
|
+
pdb_dir = os.path.join(datasets_dir, "pdb_structures")
|
|
842
|
+
uniprot_ids = os.listdir(pdb_dir)
|
|
843
|
+
uniprot_ids = [uni_id.split("-")[1] for uni_id in list(set(uniprot_ids)) if ".pdb" in uni_id]
|
|
844
|
+
logger.debug("Retrieving Uniprot ID to HUGO symbol mapping information..")
|
|
845
|
+
uniprot_to_gene_dict = uniprot_to_hugo(uniprot_ids)
|
|
846
|
+
# # Workaround if the direct request to UniprotKB stops working (it has happened temporarily)
|
|
847
|
+
# if all(pd.isna(k) for k in uniprot_to_gene_dict.keys()):
|
|
848
|
+
# logger.warning(f"Failed to retrieve Uniprot ID to HUGO symbol mapping directly from UniprotKB.")
|
|
849
|
+
# logger.warning(f"Retrying using Unipressed API client (only first HUGO symbol entry will be mapped)..")
|
|
850
|
+
# uniprot_to_gene_dict = uniprot_to_hugo_pressed(uniprot_ids)
|
|
851
|
+
|
|
852
|
+
# Get biomart metadata and canonical transcript IDs
|
|
853
|
+
ens_canonical_transcripts_lst = get_biomart_metadata(datasets_dir, uniprot_ids)
|
|
854
|
+
|
|
855
|
+
# Create a dataframe with protein sequences
|
|
856
|
+
logger.debug("Initializing sequence df..")
|
|
857
|
+
seq_df = initialize_seq_df(pdb_dir, uniprot_to_gene_dict)
|
|
858
|
+
|
|
859
|
+
if mane:
|
|
860
|
+
seq_df = process_seq_df_mane(seq_df,
|
|
861
|
+
datasets_dir,
|
|
862
|
+
uniprot_to_gene_dict,
|
|
863
|
+
ens_canonical_transcripts_lst,
|
|
864
|
+
num_cores,
|
|
865
|
+
mane_version=mane_version)
|
|
866
|
+
else:
|
|
867
|
+
seq_df = process_seq_df(seq_df,
|
|
868
|
+
datasets_dir,
|
|
869
|
+
organism,
|
|
870
|
+
uniprot_to_gene_dict,
|
|
871
|
+
ens_canonical_transcripts_lst,
|
|
872
|
+
num_cores,
|
|
873
|
+
rm_weird_chr,
|
|
874
|
+
mane_version=mane_version)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
# # Filter out non-standard chromosomes
|
|
878
|
+
# chr_lst = [str(i) for i in range(1, 23)] + ['X', 'Y', 'M']
|
|
879
|
+
# seq_df = seq_df[seq_df.Chr.isin(chr_lst) | seq_df.Chr.isna()].reset_index(drop=True)
|
|
880
|
+
|
|
881
|
+
# Save
|
|
882
|
+
seq_df_cols = ['Gene', 'HGNC_ID', 'Ens_Gene_ID',
|
|
883
|
+
'Ens_Transcr_ID', 'Uniprot_ID', 'F',
|
|
884
|
+
'Seq', 'Chr', 'Reverse_strand', 'Exons_coord',
|
|
885
|
+
'Seq_dna', 'Tri_context','Reference_info']
|
|
886
|
+
seq_df = seq_df[[col for col in seq_df_cols if col in seq_df.columns]]
|
|
887
|
+
seq_df.to_csv(output_seq_df, index=False, sep="\t")
|
|
888
|
+
logger.debug(f"Sequences dataframe saved in: {output_seq_df}")
|
|
889
|
+
|
|
890
|
+
return seq_df
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
if __name__ == "__main__":
|
|
894
|
+
OUTPUT_DS = 'test/output_datasets'
|
|
895
|
+
get_seq_df(datasets_dir=OUTPUT_DS,
|
|
896
|
+
output_seq_df=os.path.join(OUTPUT_DS, "seq_for_mut_prob.tsv"),
|
|
897
|
+
organism="Homo sapiens",
|
|
898
|
+
mane=True,
|
|
899
|
+
num_cores=8,
|
|
900
|
+
mane_version=1.3)
|