Repository navigation
Expand file tree
/
Copy pathutils.py
More file actions
251 lines (179 loc) · 8.74 KB
/
Copy pathutils.py
File metadata and controls
251 lines (179 loc) · 8.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import os
import pandas as pd
import numpy as np
import joblib
import scipy.sparse as sp
from rdkit import Chem, DataStructs
from rdkit.Chem import rdMolDescriptors, rdFingerprintGenerator
from rdkit.Chem.SaltRemover import SaltRemover
from db import Database
# ============ Train / Test Splitting ============
def _split_subclass_by_adduct_distribution(subclass_df, test_size, rng):
count_matrix = subclass_df.groupby(["smi", "adduct"]).size().unstack(fill_value=0)
smi_order = count_matrix.index.to_numpy().copy()
rng.shuffle(smi_order)
count_matrix = count_matrix.loc[smi_order]
adducts = count_matrix.columns.to_numpy()
group_adduct_counts = count_matrix.to_numpy(dtype=float)
group_sizes = group_adduct_counts.sum(axis=1)
target_props = subclass_df["adduct"].value_counts(normalize=True).reindex(adducts, fill_value=0).to_numpy()
target_test_rows = round(test_size * len(subclass_df))
n_groups = group_adduct_counts.shape[0]
remaining_mask = np.ones(n_groups, dtype=bool)
test_selected = np.zeros(n_groups, dtype=bool)
test_adduct_counts = np.zeros(len(adducts))
test_row_count = 0
while remaining_mask.sum() > 1 and test_row_count < target_test_rows:
remaining_idx = np.nonzero(remaining_mask)[0]
candidate_combined = test_adduct_counts[None, :] + group_adduct_counts[remaining_idx]
candidate_props = candidate_combined / candidate_combined.sum(axis=1, keepdims=True)
distances = np.abs(candidate_props - target_props[None, :]).sum(axis=1)
chosen_idx = remaining_idx[np.argmin(distances)]
test_adduct_counts += group_adduct_counts[chosen_idx]
test_row_count += group_sizes[chosen_idx]
test_selected[chosen_idx] = True
remaining_mask[chosen_idx] = False
groups_by_smi = dict(tuple(subclass_df.groupby("smi")))
train_groups = [groups_by_smi[smi_order[i]] for i in range(n_groups) if not test_selected[i]]
test_groups = [groups_by_smi[smi_order[i]] for i in range(n_groups) if test_selected[i]]
return train_groups, test_groups
def _split_group_by_column(group_df, column, test_size, rng):
train_parts, test_parts = [], []
for _, column_df in group_df.groupby(column):
train_groups, test_groups = _split_subclass_by_adduct_distribution(column_df, test_size, rng)
train_parts.extend(train_groups)
test_parts.extend(test_groups)
return train_parts, test_parts
def train_test_split_custom(
database_file,
test_size=0.2,
random_state=26,
use_metlin=True,
):
db = Database(database_file)
query = "SELECT smi, mass, z, ccs, name, superclass, class, subclass, adduct, tag FROM master_clean"
if not use_metlin:
query += " WHERE tag != 'METLIN'"
df = db.read_df(query)
for column in ("subclass", "class", "superclass"):
df[column] = df[column].str.replace(r" \(predicted\)$", "", regex=True)
rng = np.random.default_rng(random_state)
has_subclass = df["subclass"].notna()
has_class = df["class"].notna()
has_superclass = df["superclass"].notna()
subclass_tier = df[has_subclass]
class_tier = df[~has_subclass & has_class]
superclass_tier = df[~has_subclass & ~has_class & has_superclass]
unclassified_tier = df[~has_subclass & ~has_class & ~has_superclass]
train_parts, test_parts = [], []
for tier_df, column in ((subclass_tier, "subclass"), (class_tier, "class"), (superclass_tier, "superclass")):
tier_train, tier_test = _split_group_by_column(tier_df, column, test_size, rng)
train_parts.extend(tier_train)
test_parts.extend(tier_test)
print(f"{len(tier_df)} rows split by {column}")
train_parts.append(unclassified_tier)
print(f"{len(unclassified_tier)} rows with no superclass/class/subclass -- added to training only")
train_df = pd.concat(train_parts, ignore_index=True) if train_parts else pd.DataFrame(columns=df.columns)
test_df = pd.concat(test_parts, ignore_index=True) if test_parts else pd.DataFrame(columns=df.columns)
train_df.to_csv("train_data.csv", index=False)
test_df.to_csv("test_data.csv", index=False)
print(len(train_df), "train rows")
print(len(test_df), "test rows")
# ============ Molecular Feature Calculation ============
def calculate_base_features(smiles: str, ion_mass:float, adducts: list, adduct: str):
mol = Chem.MolFromSmiles(smiles)
mol = Chem.AddHs(mol)
molecular_weight = rdMolDescriptors.CalcExactMolWt(mol)
adduct_mass = ion_mass - molecular_weight
adduct_one_hot = [0] * (len(adducts) + 1)
adduct_index = adducts.index(adduct) if adduct in adducts else len(adducts)
adduct_one_hot[adduct_index] = 1
return np.array([molecular_weight, adduct_mass] + adduct_one_hot, dtype=float)
def calculate_sparse_fingerprint(smiles: str) -> dict:
mol = Chem.MolFromSmiles(smiles)
mol = Chem.AddHs(mol)
# no fpSize -- atom environments keep their raw id instead of colliding into a folded bit
morgan_generator = rdFingerprintGenerator.GetMorganGenerator(radius=2, includeChirality=True)
sparse_fp = morgan_generator.GetSparseCountFingerprint(mol)
return dict(sparse_fp.GetNonzeroElements())
def calculate_sparse_fingerprints(smiles_series) -> list:
return [calculate_sparse_fingerprint(smi) for smi in smiles_series]
def to_rdkit_fingerprint(fp_dict: dict):
fp = DataStructs.ULongSparseIntVect(2 ** 64 - 1)
for env_id, count in fp_dict.items():
fp[env_id] = count
return fp
# ============ Fingerprint Vocabulary + Vectorization ============
def build_fingerprint_vocabulary(fp_dicts, min_molecule_count):
doc_freq = {}
for fp in fp_dicts:
for env_id in fp:
doc_freq[env_id] = doc_freq.get(env_id, 0) + 1
return sorted(env_id for env_id, count in doc_freq.items() if count >= min_molecule_count)
def build_full_fingerprint_vocabulary(database_file):
db = Database(database_file)
unique_smiles = db.read_df("SELECT DISTINCT smi FROM master_clean")["smi"]
fp_dicts = calculate_sparse_fingerprints(unique_smiles)
return build_fingerprint_vocabulary(fp_dicts, min_molecule_count=1)
def load_or_build_fingerprint_vocabulary(database_file, fp_vocab_file):
if os.path.exists(fp_vocab_file):
return joblib.load(fp_vocab_file)
fp_vocab = build_full_fingerprint_vocabulary(database_file)
joblib.dump(fp_vocab, fp_vocab_file)
return fp_vocab
def build_fingerprint_index(fp_vocab):
return {env_id: column for column, env_id in enumerate(fp_vocab)}
def vectorize_sparse_fp(fp_dict: dict, fp_index: dict) -> np.ndarray:
vector = np.zeros(len(fp_index), dtype=np.float32)
for env_id, count in fp_dict.items():
column = fp_index.get(env_id)
if column is not None:
vector[column] = count
return vector
def vectorize_fingerprints_sparse(fp_dicts, fp_index) -> sp.csr_matrix:
indptr = [0]
indices = []
data = []
for fp in fp_dicts:
for env_id, count in fp.items():
column = fp_index.get(env_id)
if column is not None:
indices.append(column)
data.append(count)
indptr.append(len(indices))
return sp.csr_matrix((data, indices, indptr), shape=(len(fp_dicts), len(fp_index)), dtype=np.float32)
def build_feature_matrix_sparse(base_features, fp_dicts, fp_index) -> sp.csr_matrix:
return sp.hstack([
np.array(base_features, dtype=np.float32),
vectorize_fingerprints_sparse(fp_dicts, fp_index),
], format="csr")
# ============ CCS Regression Dataset Featurization ============
def featurize_ccs_dataset(df, adducts):
base_features, fp_dicts, ccs_values, metadata = [], [], [], []
for _, row in df.iterrows():
base = calculate_base_features(row["smi"], row["mass"], adducts, row["adduct"])
fp = calculate_sparse_fingerprint(row["smi"])
if base is None or fp is None:
continue
base_features.append(base)
fp_dicts.append(fp)
ccs_values.append(row["ccs"])
metadata.append((row["tag"], row["subclass"], row["adduct"], row["name"], row["smi"], row["z"]))
return base_features, fp_dicts, np.array(ccs_values, dtype=float), metadata
# ============ Miscellaneous Utils ============
def calculate_charge(adduct: str) -> int:
polarity = -1 if adduct[-1] == "-" else 1
charge = ""
for c in adduct[-2::-1]:
if c == "]":
break
charge = c + charge
if charge == "":
charge = 1
return int(charge) * polarity
def desalt_and_mass(smiles: str):
mol = Chem.MolFromSmiles(smiles)
stripped_mol = SaltRemover().StripMol(mol)
desalted_smiles = Chem.MolToSmiles(stripped_mol)
desalted_mol = Chem.MolFromSmiles(desalted_smiles)
return desalted_smiles, rdMolDescriptors.CalcExactMolWt(desalted_mol)