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,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contains function to assign clustering anomaly score and perform simulations
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import daiquiri
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from scipy import stats
|
|
9
|
+
from decimal import Decimal, getcontext
|
|
10
|
+
from functools import reduce
|
|
11
|
+
import operator
|
|
12
|
+
|
|
13
|
+
from scripts import __logger_name__
|
|
14
|
+
|
|
15
|
+
logger = daiquiri.getLogger(__logger_name__ + ".run.score_and_simulations")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def dcm_factorial(n):
|
|
19
|
+
"""
|
|
20
|
+
Compute factorial.
|
|
21
|
+
"""
|
|
22
|
+
return reduce(operator.mul, [Decimal(i) for i in range(1, int(n)+1)], Decimal(1))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def dcm_binom_coeff(n, k):
|
|
26
|
+
"""
|
|
27
|
+
Compute binomial coefficient.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
return dcm_factorial(n) / (dcm_factorial(k) * dcm_factorial(n - k))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def dcm_binom_cdf(k, n, p):
|
|
34
|
+
"""
|
|
35
|
+
Compute binomial cumulative distribution function (CDF).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
p = Decimal(p)
|
|
39
|
+
q = Decimal(1) - p
|
|
40
|
+
cdf = Decimal(0)
|
|
41
|
+
for i in range(int(k) + 1):
|
|
42
|
+
cdf += dcm_binom_coeff(n, i) * (p ** i) * (q ** (n - i))
|
|
43
|
+
|
|
44
|
+
return cdf
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def dcm_binom_sf(k, n, p):
|
|
48
|
+
"""
|
|
49
|
+
Compute binomial survival function (SF).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
return Decimal('1') - dcm_binom_cdf(k, n, p)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def dcm_binom_logsf(k, n, p):
|
|
56
|
+
"""
|
|
57
|
+
Compute log binomial survival function.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
sf = dcm_binom_sf(k, n, p)
|
|
61
|
+
if sf <= 0:
|
|
62
|
+
return np.inf
|
|
63
|
+
return sf.ln()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_dcm_anomaly_score(k, n, p, decimal=600):
|
|
67
|
+
"""
|
|
68
|
+
Use the decimal package to compute the anomaly score
|
|
69
|
+
with high precision to avoid approximation of the
|
|
70
|
+
numerator.
|
|
71
|
+
|
|
72
|
+
Score: loglik equal or larger mut_count / loglik(N)
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
getcontext().prec = decimal
|
|
76
|
+
num = dcm_binom_logsf(k-1, n, p)
|
|
77
|
+
den = stats.binom.logpmf(k=n, n=n, p=p)
|
|
78
|
+
|
|
79
|
+
return float(num / Decimal(den))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def recompute_inf_score(result_pos_df, gene_mut, vol_missense_mut_prob):
|
|
83
|
+
"""
|
|
84
|
+
Use high precision calculation to recompute the score that
|
|
85
|
+
were approximated to inf by scipy.
|
|
86
|
+
|
|
87
|
+
The issue happens in extreme cases when the numerator of the score
|
|
88
|
+
is so small that is approximated to 0.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
inf_ix = np.isinf(result_pos_df.Score)
|
|
92
|
+
if sum(inf_ix) > 0:
|
|
93
|
+
for ix, k, n, p in zip(np.where(inf_ix)[0],
|
|
94
|
+
result_pos_df.Mut_in_vol[inf_ix],
|
|
95
|
+
np.repeat(gene_mut, sum(inf_ix)),
|
|
96
|
+
vol_missense_mut_prob[inf_ix]):
|
|
97
|
+
|
|
98
|
+
if np.isinf(result_pos_df.iloc[ix].Score):
|
|
99
|
+
result_pos_df.loc[ix, "Score"] = get_dcm_anomaly_score(k, n, p)
|
|
100
|
+
else:
|
|
101
|
+
logger.warning("Trying to overwrite a non-inf score: Skipping..")
|
|
102
|
+
|
|
103
|
+
return result_pos_df
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_anomaly_score(vec_mut_in_vol, gene_mut, vec_vol_miss_mut_prob):
|
|
107
|
+
"""
|
|
108
|
+
Compute a metric that scores the anomaly of observing a certain
|
|
109
|
+
number of mutations in the volume of a residue.
|
|
110
|
+
It takes into account the volume and the mutation rate of the codon
|
|
111
|
+
of each residue within that volume.
|
|
112
|
+
|
|
113
|
+
Score: loglik equal or larger mut_count / loglik(N)
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
den = stats.binom.logpmf(k=gene_mut, n=gene_mut, p=vec_vol_miss_mut_prob)
|
|
117
|
+
|
|
118
|
+
return stats.binom.logsf(k=vec_mut_in_vol-1, n=gene_mut, p=vec_vol_miss_mut_prob) / den
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def simulate_mutations(n_mutations, p, size, seed=None):
|
|
122
|
+
"""
|
|
123
|
+
Simulate the mutations given the mutation rate of a cohort.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
rng = np.random.default_rng(seed=seed)
|
|
127
|
+
samples = rng.multinomial(n_mutations, p, size=size)
|
|
128
|
+
|
|
129
|
+
return samples
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_sim_anomaly_score(mut_count,
|
|
133
|
+
cmap,
|
|
134
|
+
gene_miss_prob,
|
|
135
|
+
vol_missense_mut_prob,
|
|
136
|
+
num_iteration=1000,
|
|
137
|
+
seed=None):
|
|
138
|
+
"""
|
|
139
|
+
Simulated mutations following the mutation profile of the cohort.
|
|
140
|
+
Compute the log-likelihood of observing k or more mutation in the
|
|
141
|
+
volume and compare it with the corresponding simualted rank.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
# Generate x sets of random mutation distributed following the mut
|
|
145
|
+
# profile of the cohort, each with the same size of the observed mut
|
|
146
|
+
mut_sim = simulate_mutations(mut_count, gene_miss_prob, num_iteration, seed)
|
|
147
|
+
|
|
148
|
+
# Get the density of mutations of each position in each iteration
|
|
149
|
+
density_sim = np.einsum('ij,jk->ki', cmap, mut_sim.T.astype(float), optimize=True)
|
|
150
|
+
|
|
151
|
+
# Compute the ranked score of the densities obtained at each iteration
|
|
152
|
+
# sign is used to sort in descending order
|
|
153
|
+
loglik_plus = -np.sort(-get_anomaly_score(density_sim, mut_count, vol_missense_mut_prob))
|
|
154
|
+
|
|
155
|
+
return pd.DataFrame(loglik_plus).T
|
scripts/run/utils.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import daiquiri
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import subprocess
|
|
7
|
+
import io
|
|
8
|
+
import gzip
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from scripts import __logger_name__
|
|
12
|
+
|
|
13
|
+
logger = daiquiri.getLogger(__logger_name__ + ".run.utils")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Parsers
|
|
17
|
+
|
|
18
|
+
def get_seq_df_input_symbols(input_df, seq_df, mane=False):
|
|
19
|
+
"""
|
|
20
|
+
Update gene names (HUGO Symbols) of O3D built sequence with names in input file.
|
|
21
|
+
Do it only for entries in the sequence df with available transcript information
|
|
22
|
+
and use transcript ID to get gene name.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Split sequence df by entries with available transcript info (Reference_info 0 and 1) and not available ones (-1)
|
|
26
|
+
seq_df_tr_missing = seq_df[seq_df["Reference_info"] == -1].reset_index(drop=True)
|
|
27
|
+
seq_df_tr_available = seq_df[seq_df["Reference_info"] != -1].reset_index(drop=True)
|
|
28
|
+
|
|
29
|
+
# Use names from input
|
|
30
|
+
df_mapping = input_df[["Hugo_Symbol", "Feature"]].rename(columns={"Hugo_Symbol" : "Gene", "Feature" : "Ens_Transcr_ID"})
|
|
31
|
+
seq_df_tr_available = seq_df_tr_available.drop(columns=["Gene"]).drop_duplicates().merge(df_mapping, how="left", on="Ens_Transcr_ID")
|
|
32
|
+
|
|
33
|
+
# If the same gene is associated to multiple structures, keep the first one obtained from Uniprot (descending, Reference_info 1) or keep the MANE (ascending, Reference_info 0)
|
|
34
|
+
# TO DO: Use the one reviewed (UniProtKB reviewed (Swiss-Prot)), if multiple Uniprot ones are present. The info must be added during the build step
|
|
35
|
+
order_ascending = [True, mane]
|
|
36
|
+
seq_df_tr_available = seq_df_tr_available.sort_values(by=["Gene", "Reference_info"], ascending=order_ascending).drop_duplicates(subset="Gene")
|
|
37
|
+
|
|
38
|
+
# If the same genes is associated to multiple structures, keep the one not obtained by Backtranseq (Reference_info 1 or 0)
|
|
39
|
+
seq_df = pd.concat([seq_df_tr_missing, seq_df_tr_available]).sort_values(by=["Gene", "Reference_info"], ascending=[True, False])
|
|
40
|
+
|
|
41
|
+
return seq_df.drop_duplicates(subset="Gene").reset_index(drop=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_hgvsp_mut(df_row):
|
|
45
|
+
"""
|
|
46
|
+
Parse mutation entries to get HGVSp_Short format.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
amino_acids = df_row["Amino_acids"]
|
|
50
|
+
|
|
51
|
+
if pd.isna(amino_acids):
|
|
52
|
+
return np.nan
|
|
53
|
+
|
|
54
|
+
amino_acids = amino_acids.split("/")
|
|
55
|
+
if len(amino_acids) > 1:
|
|
56
|
+
return f"p.{amino_acids[0]}{df_row['Protein_position']}{amino_acids[1]}"
|
|
57
|
+
|
|
58
|
+
return np.nan
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def filter_transcripts(df, seq_df):
|
|
62
|
+
"""
|
|
63
|
+
Filter VEP output by Oncodrive3D transcripts. For genes with NA
|
|
64
|
+
transcripts in the sequence dataframe, keep canonical ones.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
if "CANONICAL" in df.columns and "Feature" in df.columns:
|
|
68
|
+
|
|
69
|
+
# Genes without available transcript info in O3D built datasets
|
|
70
|
+
df_tr_missing = df[df["Hugo_Symbol"].isin(seq_df.loc[seq_df["Reference_info"] == -1, "Gene"])]
|
|
71
|
+
df_tr_missing = df_tr_missing[df_tr_missing["CANONICAL"] == "YES"]
|
|
72
|
+
|
|
73
|
+
# Genes with transcript info
|
|
74
|
+
df_tr_available = df[df["Feature"].isin(seq_df.loc[seq_df["Reference_info"] != -1, "Ens_Transcr_ID"])]
|
|
75
|
+
|
|
76
|
+
return pd.concat((df_tr_available, df_tr_missing))
|
|
77
|
+
|
|
78
|
+
else:
|
|
79
|
+
logger.critical("Failed to filter input by O3D transcripts. Please provide as input the output of VEP with canonical and transcripts information: Exiting..")
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parse_vep_output(df,
|
|
84
|
+
seq_df=None,
|
|
85
|
+
use_o3d_transcripts=False,
|
|
86
|
+
use_input_symbols=False,
|
|
87
|
+
mane=False):
|
|
88
|
+
"""
|
|
89
|
+
Parse the dataframe in case it is the direct output of VEP without any
|
|
90
|
+
processing. Rename the columns to match the fields name of a MAF file,
|
|
91
|
+
and select the canonical transcripts if multiple ones are present.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
df.rename(columns={"SYMBOL": "Hugo_Symbol",
|
|
95
|
+
"Consequence": "Variant_Classification"}, inplace=True)
|
|
96
|
+
|
|
97
|
+
# Adapt HUGO_Symbol in seq_df to input file
|
|
98
|
+
if seq_df is not None and use_input_symbols:
|
|
99
|
+
logger.debug("Adapting Oncodrive3D HUGO Symbols of built datasets to input file..")
|
|
100
|
+
seq_df = get_seq_df_input_symbols(df, seq_df, mane)
|
|
101
|
+
|
|
102
|
+
# Transcripts filtering
|
|
103
|
+
if use_o3d_transcripts and seq_df is not None:
|
|
104
|
+
logger.debug("Filtering input by Oncodrive3D built transcripts..")
|
|
105
|
+
df = filter_transcripts(df, seq_df)
|
|
106
|
+
elif "CANONICAL" in df.columns:
|
|
107
|
+
df = df[df["CANONICAL"] == "YES"]
|
|
108
|
+
|
|
109
|
+
# Get HGVSp
|
|
110
|
+
if "HGVSp_Short" not in df.columns and "Amino_acids" in df.columns and "Protein_position" in df.columns:
|
|
111
|
+
df["HGVSp_Short"] = df.apply(get_hgvsp_mut, axis=1)
|
|
112
|
+
|
|
113
|
+
return df, seq_df
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parse_mutations(maf):
|
|
117
|
+
"""
|
|
118
|
+
Parse HGVSp_Short in maf.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
# Ensure the required 'HGVSp_Short' column is present and not empty
|
|
122
|
+
if 'HGVSp_Short' not in maf.columns or maf['HGVSp_Short'].isnull().all():
|
|
123
|
+
logger.critical("Missing or empty 'HGVSp_Short' column in input MAF data.")
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
|
|
126
|
+
# Parse the position, wild type, and mutation type from 'HGVSp_Short'
|
|
127
|
+
maf.dropna(subset="HGVSp_Short", inplace=True)
|
|
128
|
+
maf['Pos'] = maf['HGVSp_Short'].apply(lambda x: re.sub(r"\D", "", x)).astype(np.int32)
|
|
129
|
+
maf['WT'] = maf['HGVSp_Short'].apply(lambda x: re.findall(r"\D", x)[2])
|
|
130
|
+
maf['Mut'] = maf['HGVSp_Short'].apply(lambda x: re.findall(r"\D", x)[3])
|
|
131
|
+
|
|
132
|
+
# Parse cols
|
|
133
|
+
columns_to_keep = ['Hugo_Symbol', 'Pos', 'WT', 'Mut', 'Tumor_Sample_Barcode', 'Feature', 'Transcript_ID']
|
|
134
|
+
columns_to_keep = [col for col in columns_to_keep if col in maf.columns]
|
|
135
|
+
maf = maf[columns_to_keep]
|
|
136
|
+
maf = maf.rename(columns={'Hugo_Symbol' : 'Gene', 'Feature': 'Transcript_ID'})
|
|
137
|
+
|
|
138
|
+
return maf.sort_values(by=['Gene', 'Pos']).reset_index(drop=True)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def add_transcript_info(maf, seq_df):
|
|
142
|
+
"""
|
|
143
|
+
Add transcript status information.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
if 'Transcript_ID' not in maf.columns:
|
|
147
|
+
maf['Transcript_ID'] = np.nan
|
|
148
|
+
maf = maf.merge(seq_df[[col for col in ['Gene', 'Ens_Transcr_ID', 'Refseq_prot'] if col in seq_df.columns]].drop_duplicates(),
|
|
149
|
+
on='Gene', how='left').rename(columns={"Ens_Transcr_ID" : "O3D_transcript_ID"})
|
|
150
|
+
|
|
151
|
+
# Vectorized conditions for setting Transcript_status
|
|
152
|
+
conditions = [
|
|
153
|
+
maf['Transcript_ID'].isna(),
|
|
154
|
+
maf['O3D_transcript_ID'].isna(),
|
|
155
|
+
maf['Transcript_ID'] != maf['O3D_transcript_ID'],
|
|
156
|
+
maf['Transcript_ID'] == maf['O3D_transcript_ID']
|
|
157
|
+
]
|
|
158
|
+
choices = ['Input_missing', 'O3D_missing', 'Mismatch', 'Match']
|
|
159
|
+
maf['Transcript_status'] = np.select(conditions, choices, default=np.nan)
|
|
160
|
+
|
|
161
|
+
# Log transcript report
|
|
162
|
+
transcript_report = maf['Transcript_status'].value_counts().reset_index(name='Count')
|
|
163
|
+
transcript_report = ", ".join([f"{status} = {count}" for status, count in transcript_report.to_numpy()])
|
|
164
|
+
logger.info(f"Transcript status of {len(maf)} mutations: {transcript_report}")
|
|
165
|
+
|
|
166
|
+
return maf
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def read_input(input_path):
|
|
170
|
+
"""
|
|
171
|
+
Read input file optimizing memory usage.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
cols_to_read = ["Variant_Classification",
|
|
175
|
+
"Tumor_Sample_Barcode",
|
|
176
|
+
"Feature",
|
|
177
|
+
"Transcript_ID",
|
|
178
|
+
"Consequence",
|
|
179
|
+
"SYMBOL",
|
|
180
|
+
"Hugo_Symbol",
|
|
181
|
+
"CANONICAL",
|
|
182
|
+
"HGVSp_Short",
|
|
183
|
+
"Amino_acids",
|
|
184
|
+
"Protein_position"]
|
|
185
|
+
|
|
186
|
+
header = pd.read_table(input_path, nrows=0)
|
|
187
|
+
cols_to_read = [col for col in cols_to_read if col in header.columns]
|
|
188
|
+
dtype_mapping = {col : "object" for col in cols_to_read}
|
|
189
|
+
dtype = {key: dtype_mapping[key] for key in cols_to_read if key in dtype_mapping}
|
|
190
|
+
|
|
191
|
+
return pd.read_table(input_path, usecols=cols_to_read, dtype=dtype)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def parse_maf_input(input_path, seq_df=None, use_o3d_transcripts=False, use_input_symbols=False, mane=False):
|
|
195
|
+
"""
|
|
196
|
+
Parsing and process MAF input data.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
# Load, parse from VEP and update seq_df if needed
|
|
200
|
+
logger.info(f"Reading input mutations file..")
|
|
201
|
+
maf = read_input(input_path)
|
|
202
|
+
logger.debug(f"Processing [{len(maf)}] total mutations..")
|
|
203
|
+
maf, seq_df = parse_vep_output(maf, seq_df, use_o3d_transcripts, use_input_symbols, mane)
|
|
204
|
+
|
|
205
|
+
# Extract and parse missense mutations
|
|
206
|
+
maf = maf[maf['Variant_Classification'].str.contains('Missense_Mutation|missense_variant')]
|
|
207
|
+
if "Protein_position" in maf.columns:
|
|
208
|
+
maf = maf[~maf['Protein_position'].astype(str).str.contains('-')] # Filter DBS
|
|
209
|
+
logger.debug(f"Processing [{len(maf)}] missense mutations..")
|
|
210
|
+
maf = parse_mutations(maf)
|
|
211
|
+
|
|
212
|
+
# Add transcript status from seq_df
|
|
213
|
+
if seq_df is not None:
|
|
214
|
+
maf = add_transcript_info(maf, seq_df)
|
|
215
|
+
|
|
216
|
+
return maf.reset_index(drop=True), seq_df
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## Other utils
|
|
220
|
+
|
|
221
|
+
def get_gene_entry(data, genes, entry):
|
|
222
|
+
|
|
223
|
+
return [data.loc[data["Gene"] == gene, entry].values[0] for gene in genes]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def weighted_avg_plddt_vol(target_pos, mut_plddt_df, cmap):
|
|
227
|
+
"""
|
|
228
|
+
Get the weighted average pLDDT score across residues in
|
|
229
|
+
the volume, based on number of mutations hitting each residue.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
return mut_plddt_df[[pos in np.where(cmap[target_pos-1])[0]+1 for pos in mut_plddt_df.Pos]].Confidence.mean()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def weighted_avg_pae_vol(target_pos, mut_plddt_df, cmap, pae):
|
|
236
|
+
"""
|
|
237
|
+
Get the weighted average PAE across residues in the volume,
|
|
238
|
+
based on number of mutations hitting each residue.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
contacts = mut_plddt_df[[pos in np.where(cmap[target_pos-1])[0]+1 for pos in mut_plddt_df.Pos]].Pos.values
|
|
242
|
+
pae_vol = pae[np.repeat(target_pos-1, len(contacts)), contacts-1].mean()
|
|
243
|
+
|
|
244
|
+
return pae_vol
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def get_samples_info(mut_gene_df, cmap):
|
|
248
|
+
"""
|
|
249
|
+
Get total samples and ratio of unique samples having
|
|
250
|
+
mutations in the volume of each mutated residues.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
# Get total samples and # mutated samples of each mutated res
|
|
254
|
+
tot_samples = len(mut_gene_df["Tumor_Sample_Barcode"].unique())
|
|
255
|
+
pos_barcodes = mut_gene_df.groupby("Pos").apply(lambda x: x["Tumor_Sample_Barcode"].unique())
|
|
256
|
+
pos_barcodes = pos_barcodes.reset_index().rename(columns={0 : "Barcode"})
|
|
257
|
+
|
|
258
|
+
# Get the ratio of unique samples with mut in the vol of each mutated res
|
|
259
|
+
uniq_pos_barcodes = [len(pos_barcodes[[pos in np.where(cmap[i-1])[0]+1 for
|
|
260
|
+
pos in pos_barcodes.Pos]].Barcode.explode().unique()) for i in pos_barcodes.Pos]
|
|
261
|
+
pos_barcodes["Tot_samples"] = tot_samples
|
|
262
|
+
pos_barcodes["Samples_in_vol"] = uniq_pos_barcodes
|
|
263
|
+
#pos_barcodes["Ratio_samples_in_vol"] = np.array(uniq_pos_barcodes) / tot_samples
|
|
264
|
+
|
|
265
|
+
return pos_barcodes
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_unique_pos_in_contact(lst_pos, cmap):
|
|
269
|
+
"""
|
|
270
|
+
Given a list of position and a contact map, return a numpy
|
|
271
|
+
array of unique positions in contact with the given ones.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
return np.unique(np.concatenate([np.where(cmap[pos-1])[0]+1 for pos in lst_pos]))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def add_info(mut_gene_df, result_pos_df, cmap, pae=None, sample_info=False):
|
|
278
|
+
"""
|
|
279
|
+
Add information about the ratio of unique samples in the volume of
|
|
280
|
+
each mutated residues and in each detected community (meta-cluster)
|
|
281
|
+
to the residues-level output of the tool.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
# Add sample info
|
|
285
|
+
if sample_info:
|
|
286
|
+
if "Tumor_Sample_Barcode" in mut_gene_df.columns:
|
|
287
|
+
samples_info = get_samples_info(mut_gene_df, cmap)
|
|
288
|
+
result_pos_df = result_pos_df.merge(samples_info.drop(columns=["Barcode"]), on="Pos", how="outer")
|
|
289
|
+
else:
|
|
290
|
+
result_pos_df["Tot_samples"] = np.nan
|
|
291
|
+
result_pos_df["Samples_in_vol"] = np.nan
|
|
292
|
+
|
|
293
|
+
# Get per-community info
|
|
294
|
+
if result_pos_df["Clump"].isna().all():
|
|
295
|
+
if sample_info:
|
|
296
|
+
result_pos_df["Samples_in_cl_vol"] = np.nan
|
|
297
|
+
result_pos_df["Mut_in_cl_vol"] = np.nan
|
|
298
|
+
result_pos_df["Res_in_cl"] = np.nan
|
|
299
|
+
result_pos_df["pLDDT_cl_vol"] = np.nan
|
|
300
|
+
else:
|
|
301
|
+
community_pos = result_pos_df.groupby("Clump").apply(lambda x: x.Pos.values)
|
|
302
|
+
community_mut = community_pos.apply(lambda x: sum([pos in get_unique_pos_in_contact(x, cmap) for
|
|
303
|
+
pos in mut_gene_df.Pos]))
|
|
304
|
+
community_plddt = community_pos.apply(lambda x: mut_gene_df.Confidence[[pos in get_unique_pos_in_contact(x, cmap)
|
|
305
|
+
for pos in mut_gene_df.Pos]].mean())
|
|
306
|
+
community_pos_count = community_pos.apply(lambda x: len(x))
|
|
307
|
+
|
|
308
|
+
community_info = pd.DataFrame({"Mut_in_cl_vol" : community_mut,
|
|
309
|
+
"Res_in_cl" : community_pos_count,
|
|
310
|
+
"pLDDT_cl_vol" : np.round(community_plddt, 2)})
|
|
311
|
+
|
|
312
|
+
if sample_info:
|
|
313
|
+
if "Tumor_Sample_Barcode" in mut_gene_df.columns:
|
|
314
|
+
community_samples = community_pos.apply(lambda x:
|
|
315
|
+
len(mut_gene_df[[pos in get_unique_pos_in_contact(x, cmap) for
|
|
316
|
+
pos in mut_gene_df.Pos]].Tumor_Sample_Barcode.unique()))
|
|
317
|
+
else:
|
|
318
|
+
community_samples = np.nan
|
|
319
|
+
community_info["Samples_in_cl_vol"] = community_samples
|
|
320
|
+
|
|
321
|
+
# Add to residues-level result
|
|
322
|
+
result_pos_df = result_pos_df.merge(community_info, on="Clump", how="outer")
|
|
323
|
+
|
|
324
|
+
# AF PAE
|
|
325
|
+
if pae is not None:
|
|
326
|
+
result_pos_df["PAE_vol"] = np.round(result_pos_df.apply(lambda x: weighted_avg_pae_vol(x["Pos"], mut_gene_df, cmap, pae), axis=1), 2)
|
|
327
|
+
else:
|
|
328
|
+
result_pos_df["PAE_vol"] = np.nan
|
|
329
|
+
|
|
330
|
+
# AF confidence
|
|
331
|
+
result_pos_df["pLDDT_res"] = result_pos_df.apply(lambda x: mut_gene_df.Confidence[mut_gene_df["Pos"] == x.Pos].values[0]
|
|
332
|
+
if len(mut_gene_df.Confidence[mut_gene_df["Pos"] == x.Pos].values > 0) else np.nan, axis=1)
|
|
333
|
+
result_pos_df["pLDDT_vol"] = np.round(result_pos_df.apply(lambda x: weighted_avg_plddt_vol(x["Pos"], mut_gene_df, cmap), axis=1), 2)
|
|
334
|
+
result_pos_df["pLDDT_cl_vol"] = result_pos_df.pop("pLDDT_cl_vol")
|
|
335
|
+
|
|
336
|
+
# WT AA mismatches
|
|
337
|
+
if "WT_mismatch" in mut_gene_df:
|
|
338
|
+
result_pos_df["WT_mismatch"] = result_pos_df.apply(lambda x: sum(mut_gene_df[mut_gene_df["Pos"] == x.Pos].WT_mismatch.dropna()), axis=1)
|
|
339
|
+
|
|
340
|
+
# Sort positions
|
|
341
|
+
result_pos_df = result_pos_df.sort_values("Rank").reset_index(drop=True)
|
|
342
|
+
|
|
343
|
+
return result_pos_df
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def add_nan_clust_cols(result_gene, sample_info=False):
|
|
347
|
+
"""
|
|
348
|
+
Add columns showing clustering results with only NA for
|
|
349
|
+
genes that are not tested (not enough mutations, etc).
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
result_gene = result_gene.copy()
|
|
353
|
+
|
|
354
|
+
columns = ["pval",
|
|
355
|
+
"qval",
|
|
356
|
+
"C_gene",
|
|
357
|
+
"C_pos",
|
|
358
|
+
'C_label',
|
|
359
|
+
'Score_obs_sim_top_vol',
|
|
360
|
+
"Clust_res",
|
|
361
|
+
'Clust_mut',
|
|
362
|
+
'Pos_top_vol',
|
|
363
|
+
'Mut_in_top_vol',
|
|
364
|
+
"Mut_in_top_cl_vol",
|
|
365
|
+
"PAE_top_vol",
|
|
366
|
+
"pLDDT_top_vol",
|
|
367
|
+
"pLDDT_top_cl_vol",
|
|
368
|
+
'F']
|
|
369
|
+
|
|
370
|
+
if sample_info:
|
|
371
|
+
columns.extend(['Tot_samples',
|
|
372
|
+
'Samples_in_top_vol',
|
|
373
|
+
'Samples_in_top_cl_vol'])
|
|
374
|
+
|
|
375
|
+
for col in columns:
|
|
376
|
+
result_gene[col] = np.nan
|
|
377
|
+
|
|
378
|
+
return result_gene
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def sort_cols(result_gene):
|
|
382
|
+
"""
|
|
383
|
+
Simply change the order of columns of the genes-level result.
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
cols = ['Gene',
|
|
387
|
+
'Uniprot_ID',
|
|
388
|
+
'pval',
|
|
389
|
+
'qval',
|
|
390
|
+
'C_gene',
|
|
391
|
+
'C_pos',
|
|
392
|
+
'C_label',
|
|
393
|
+
'Pos_top_vol',
|
|
394
|
+
'Score_obs_sim_top_vol',
|
|
395
|
+
'Mut_in_gene',
|
|
396
|
+
'Clust_mut',
|
|
397
|
+
"Clust_res",
|
|
398
|
+
'Mut_in_top_vol',
|
|
399
|
+
"Mut_in_top_cl_vol",
|
|
400
|
+
'Tot_samples',
|
|
401
|
+
'Samples_in_top_vol',
|
|
402
|
+
'Samples_in_top_cl_vol',
|
|
403
|
+
"PAE_top_vol",
|
|
404
|
+
"pLDDT_top_vol",
|
|
405
|
+
"pLDDT_top_cl_vol",
|
|
406
|
+
'Ratio_not_in_structure',
|
|
407
|
+
'Ratio_WT_mismatch',
|
|
408
|
+
'Mut_zero_mut_prob',
|
|
409
|
+
'Pos_zero_mut_prob',
|
|
410
|
+
'Ratio_mut_zero_prob',
|
|
411
|
+
'Cancer',
|
|
412
|
+
'Cohort',
|
|
413
|
+
'F',
|
|
414
|
+
'Transcript_ID',
|
|
415
|
+
'O3D_transcript_ID',
|
|
416
|
+
'Transcript_status',
|
|
417
|
+
# 'HGNC_ID',
|
|
418
|
+
# 'Refseq_prot',
|
|
419
|
+
# 'Ens_Gene_ID'
|
|
420
|
+
'Status']
|
|
421
|
+
|
|
422
|
+
return result_gene[[col for col in cols if col in result_gene.columns]]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def empty_result_pos(sample_info=False):
|
|
426
|
+
"""
|
|
427
|
+
Get an empty position-level result of the clustering method.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
cols = ['Gene',
|
|
431
|
+
'Uniprot_ID',
|
|
432
|
+
'Pos',
|
|
433
|
+
'Mut_in_gene',
|
|
434
|
+
'Mut_in_res',
|
|
435
|
+
'Mut_in_vol',
|
|
436
|
+
'Score',
|
|
437
|
+
'Score_obs_sim',
|
|
438
|
+
'pval',
|
|
439
|
+
'C',
|
|
440
|
+
'C_ext',
|
|
441
|
+
'Clump',
|
|
442
|
+
'Rank',
|
|
443
|
+
'Tot_samples',
|
|
444
|
+
'Samples_in_vol',
|
|
445
|
+
'Samples_in_cl_vol',
|
|
446
|
+
'Mut_in_cl_vol',
|
|
447
|
+
'Res_in_cl',
|
|
448
|
+
'PAE_vol',
|
|
449
|
+
'pLDDT_res',
|
|
450
|
+
'pLDDT_vol',
|
|
451
|
+
'pLDDT_cl_vol',
|
|
452
|
+
'Cancer',
|
|
453
|
+
'Cohort']
|
|
454
|
+
|
|
455
|
+
df = pd.DataFrame(columns=cols)
|
|
456
|
+
if not sample_info:
|
|
457
|
+
df = df.drop(columns=['Tot_samples',
|
|
458
|
+
'Samples_in_vol',
|
|
459
|
+
'Samples_in_cl_vol'])
|
|
460
|
+
|
|
461
|
+
return df
|