hogprof 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hogprof/__init__.py +5 -0
- hogprof/cli.py +484 -0
- hogprof/extract_levels_hits.py +518 -0
- hogprof/generate_jaccard_distr.py +516 -0
- hogprof/lshbuilder.py +710 -0
- hogprof/metadata.py +126 -0
- hogprof/presets.py +21 -0
- hogprof/profiler.py +333 -0
- hogprof/splitter.py +53 -0
- hogprof/utils/__init__.py +1 -0
- hogprof/utils/goautils.py +229 -0
- hogprof/utils/hashutils.py +190 -0
- hogprof/utils/orthoxml.py +83 -0
- hogprof/utils/phylo.py +305 -0
- hogprof/utils/pyhamutils.py +256 -0
- hogprof-0.3.0.dist-info/METADATA +231 -0
- hogprof-0.3.0.dist-info/RECORD +21 -0
- hogprof-0.3.0.dist-info/WHEEL +5 -0
- hogprof-0.3.0.dist-info/entry_points.txt +3 -0
- hogprof-0.3.0.dist-info/licenses/LICENSE +21 -0
- hogprof-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
#import profiler
|
|
2
|
+
print("\nNote: this script requires the hogprof environment\n")
|
|
3
|
+
import argparse
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import pickle
|
|
8
|
+
from time import time
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import traceback
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
import numpy as np
|
|
13
|
+
from scipy.stats import gumbel_r, kstest, beta, probplot, gaussian_kde
|
|
14
|
+
import statsmodels.api as sm
|
|
15
|
+
import glob
|
|
16
|
+
from statsmodels.distributions.empirical_distribution import ECDF
|
|
17
|
+
from ete3 import NCBITaxa
|
|
18
|
+
ncbi = NCBITaxa()
|
|
19
|
+
|
|
20
|
+
### get current time
|
|
21
|
+
start_time_secs = time()
|
|
22
|
+
start_time = datetime.fromtimestamp(start_time_secs).strftime('%Y-%m-%d %H:%M:%S')
|
|
23
|
+
### print start time human readable
|
|
24
|
+
print(f"Script started at: {start_time}\n")
|
|
25
|
+
|
|
26
|
+
def add_profiler_path(profiler_path):
|
|
27
|
+
"""Add the directory containing profiler.py to Python's sys.path."""
|
|
28
|
+
if not os.path.isfile(profiler_path):
|
|
29
|
+
raise FileNotFoundError(
|
|
30
|
+
f"Could not find profiler.py at: {profiler_path}"
|
|
31
|
+
)
|
|
32
|
+
profiler_dir = os.path.dirname(os.path.abspath(profiler_path))
|
|
33
|
+
if profiler_dir not in sys.path:
|
|
34
|
+
sys.path.append(profiler_dir)
|
|
35
|
+
|
|
36
|
+
def create_directory(dirpath):
|
|
37
|
+
if not os.path.exists(dirpath):
|
|
38
|
+
os.mkdir(dirpath)
|
|
39
|
+
return dirpath
|
|
40
|
+
|
|
41
|
+
'''here expect index of df to have subhog ids and a column taxid'''
|
|
42
|
+
def create_bins(hogmetadata_df, outputdir, taxidmapper={}, samplesize=21):
|
|
43
|
+
### reverse dict
|
|
44
|
+
idmapper = {v: k for k, v in taxidmapper.items()}
|
|
45
|
+
### add taxid column to df from index prefix
|
|
46
|
+
hogmetadata_df['taxid'] = hogmetadata_df.index.to_series().apply(lambda x: str(x).split('_')[1], None)
|
|
47
|
+
print(hogmetadata_df.head())
|
|
48
|
+
### sort by taxid - is this problematic for random sampling????????????????
|
|
49
|
+
hogmetadata_df.sort_values(by='taxid', inplace=True)
|
|
50
|
+
# set seed
|
|
51
|
+
np.random.seed(42)
|
|
52
|
+
# Create a new DataFrame to hold the samples
|
|
53
|
+
pairs_df = pd.DataFrame(columns=['subhog'])
|
|
54
|
+
# Create a new DataFrame to hold the info about the chosen subhogs
|
|
55
|
+
subhogs_samples_info_df = pd.DataFrame(columns=['taxid', 'clade','subhog']) ###used to have proteins_num
|
|
56
|
+
#print(species_bins)
|
|
57
|
+
subhogs_samples_info_list = []
|
|
58
|
+
samples_list = []
|
|
59
|
+
for taxid_bin in hogmetadata_df['taxid'].unique():
|
|
60
|
+
# Get the subhogs in the current species bin
|
|
61
|
+
subhogs_in_bin = hogmetadata_df[hogmetadata_df['taxid'] == taxid_bin]
|
|
62
|
+
# If there are at least 2 subhogs, sample pairs
|
|
63
|
+
if len(subhogs_in_bin) >= 2:
|
|
64
|
+
try:
|
|
65
|
+
sample = subhogs_in_bin.sample(n=samplesize, replace=False) ### sample 21 for allvsall, 200 for pairs
|
|
66
|
+
except ValueError as e:
|
|
67
|
+
print(f"Error sampling for taxid bin {taxid_bin}: {e}")
|
|
68
|
+
sample = subhogs_in_bin
|
|
69
|
+
print(f" Sampled {len(sample)} subhogs for taxid_bin {taxid_bin}")
|
|
70
|
+
for index, row in sample.iterrows():
|
|
71
|
+
subhogs_samples_info_list.append({
|
|
72
|
+
'taxid': taxid_bin,
|
|
73
|
+
'subhog': index
|
|
74
|
+
})
|
|
75
|
+
# Create rows for later
|
|
76
|
+
for i in range(0, len(sample)):
|
|
77
|
+
samples_list.append({
|
|
78
|
+
'subhog': sample.index[i]
|
|
79
|
+
})
|
|
80
|
+
# Convert the lists to DataFrames
|
|
81
|
+
subhogs_samples_info_df = pd.DataFrame(subhogs_samples_info_list)
|
|
82
|
+
pairs_df = pd.DataFrame(samples_list)
|
|
83
|
+
print(pairs_df.head())
|
|
84
|
+
### save to csv files
|
|
85
|
+
subhogs_samples_info_df.to_csv(os.path.join(outputdir, "subhogs_samples_info.csv"), index=True)
|
|
86
|
+
pairs_df.to_csv(os.path.join(outputdir, "subhogs_sampled_pairs.csv"), index=False)
|
|
87
|
+
print(f"Saved subhogs_samples_info_df to {os.path.join(outputdir, 'subhogs_samples_info.csv')}")
|
|
88
|
+
print(f"Saved pairs_df to {os.path.join(outputdir, 'subhogs_sampled_pairs.csv')}")
|
|
89
|
+
return idmapper
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_hog_to_hog_jaccards(subhogs_sampled_pairs_df, lshforestfile, hashes_h5, treefile,
|
|
93
|
+
fam2orthoxmlfile, outputdir, profiler_path, allvsall=False, suffix=""):
|
|
94
|
+
### import profiler
|
|
95
|
+
add_profiler_path(profiler_path)
|
|
96
|
+
import profiler
|
|
97
|
+
|
|
98
|
+
### check for NaN, there should not be any
|
|
99
|
+
if not allvsall:
|
|
100
|
+
if subhogs_sampled_pairs_df['subhog1'].isnull().any() or subhogs_sampled_pairs_df['subhog2'].isnull().any():
|
|
101
|
+
print("Error: Some subhog IDs could not be matched.")
|
|
102
|
+
print(f"Subhogs sampled pairs DataFrame has {subhogs_sampled_pairs_df['subhog1'].isnull().sum()} null subhog1 and {subhogs_sampled_pairs_df['subhog2'].isnull().sum()} null subhog2.")
|
|
103
|
+
print(subhogs_sampled_pairs_df[subhogs_sampled_pairs_df['subhog1'].isnull() | subhogs_sampled_pairs_df['subhog2'].isnull()])
|
|
104
|
+
### remove those rows
|
|
105
|
+
subhogs_sampled_pairs_df = subhogs_sampled_pairs_df.dropna(subset=['subhog1', 'subhog2'])
|
|
106
|
+
#exit()
|
|
107
|
+
else:
|
|
108
|
+
if subhogs_sampled_pairs_df['subhog'].isnull().any():
|
|
109
|
+
print("Error: Some subhog IDs could not be matched.")
|
|
110
|
+
print(f"Subhogs sampled pairs DataFrame has {subhogs_sampled_pairs_df['subhog'].isnull().sum()} null subhogs")
|
|
111
|
+
print(subhogs_sampled_pairs_df[subhogs_sampled_pairs_df['subhog1'].isnull()])
|
|
112
|
+
print(subhogs_sampled_pairs_df[subhogs_sampled_pairs_df['subhog2'].isnull()])
|
|
113
|
+
### remove those rows
|
|
114
|
+
subhogs_sampled_pairs_df = subhogs_sampled_pairs_df.dropna(subset=['subhog'])
|
|
115
|
+
#exit()
|
|
116
|
+
|
|
117
|
+
### if already exists, just read
|
|
118
|
+
if os.path.exists(os.path.join(outputdir, f"allvsall_hashmat_{suffix}.npy")):
|
|
119
|
+
print(f"allvsall_hashmat_{suffix}.npy already exists, loading from file.")
|
|
120
|
+
hashmat = np.load(os.path.join(outputdir, f"allvsall_hashmat_{suffix}.npy"))
|
|
121
|
+
return hashmat
|
|
122
|
+
|
|
123
|
+
### read lshforest
|
|
124
|
+
p = profiler.Profiler(lshforestpath = lshforestfile,
|
|
125
|
+
hashes_h5= hashes_h5,
|
|
126
|
+
#mat_path= fam2orthoxmlfile ,
|
|
127
|
+
oma = False ,
|
|
128
|
+
nsamples = 256 ,
|
|
129
|
+
mastertree = treefile,
|
|
130
|
+
slicesubhogs = True
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if not allvsall:
|
|
134
|
+
### get jaccard similarity for each pair using hog_v_hog(self, hogs)
|
|
135
|
+
print("Calculating Jaccard similarity for each pair of subhogs...")
|
|
136
|
+
jaccard_results = []
|
|
137
|
+
for index, row in subhogs_sampled_pairs_df.iterrows():
|
|
138
|
+
subhog1 = row['subhog1']
|
|
139
|
+
subhog2 = row['subhog2']
|
|
140
|
+
try:
|
|
141
|
+
jaccard_value = p.hog_v_hog([subhog1, subhog2])
|
|
142
|
+
jaccard_results.append({
|
|
143
|
+
'subhog1': subhog1,
|
|
144
|
+
'subhog2': subhog2,
|
|
145
|
+
'jaccard': jaccard_value
|
|
146
|
+
})
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(f"Error calculating Jaccard for {subhog1} and {subhog2}: {e}")
|
|
149
|
+
jaccard_results.append({
|
|
150
|
+
'subhog1': subhog1,
|
|
151
|
+
'subhog2': subhog2,
|
|
152
|
+
'jaccard': None
|
|
153
|
+
})
|
|
154
|
+
# Convert results to DataFrame
|
|
155
|
+
jaccard_df = pd.DataFrame(jaccard_results)
|
|
156
|
+
# Save to CSV
|
|
157
|
+
jaccard_df.to_csv(os.path.join(outputdir, f"sampled_jaccard_results_{suffix}.csv"), index=False)
|
|
158
|
+
print(f"Jaccard results saved to {os.path.join(outputdir, f'sampled_jaccard_results_{suffix}.csv')}\n")
|
|
159
|
+
else:
|
|
160
|
+
### use pull_hashes and hog_v_hog method
|
|
161
|
+
print("Calculating Jaccard similarity for all pairs of subhogs...")
|
|
162
|
+
jaccard_results = []
|
|
163
|
+
try:
|
|
164
|
+
subhogs_list = pd.unique(
|
|
165
|
+
pd.concat([subhogs_sampled_pairs_df['subhog1'], subhogs_sampled_pairs_df['subhog2']])
|
|
166
|
+
)
|
|
167
|
+
except:
|
|
168
|
+
subhogs_list = pd.unique(subhogs_sampled_pairs_df['subhog'])
|
|
169
|
+
hashes_dict = p.pull_hashes(subhogs_list)
|
|
170
|
+
hashmat = p.allvall_hashes(hashes_dict)
|
|
171
|
+
# Mask out the diagonal (self-self hits)
|
|
172
|
+
allvsall_flat = hashmat[~np.eye(hashmat.shape[0], dtype=bool)].flatten()
|
|
173
|
+
### save np table to file
|
|
174
|
+
np.save(os.path.join(outputdir, f"allvsall_hashmat_{suffix}.npy"), allvsall_flat)
|
|
175
|
+
return allvsall_flat
|
|
176
|
+
|
|
177
|
+
def get_jaccard_threshold(jaccard_values, alpha=0.05):
|
|
178
|
+
"""
|
|
179
|
+
Returns the Jaccard similarity value above which p < alpha.
|
|
180
|
+
I.e., the (1 - alpha) quantile of the values.
|
|
181
|
+
|
|
182
|
+
Parameters:
|
|
183
|
+
jaccard_values (array-like): Array of Jaccard similarity values.
|
|
184
|
+
alpha (float): Significance level (default 0.05).
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
float: Jaccard threshold value, or None if input is empty.
|
|
188
|
+
"""
|
|
189
|
+
if len(jaccard_values) == 0:
|
|
190
|
+
return None
|
|
191
|
+
return np.quantile(jaccard_values, 1 - alpha)
|
|
192
|
+
|
|
193
|
+
def get_bins_jaccards_pairwise(samples_info_df, bin_name, bin_dict, subhogs_sampled_pairs_df, lshforestfile, hashes_h5, treefile, outputdir,
|
|
194
|
+
profiler_path):
|
|
195
|
+
### import profiler
|
|
196
|
+
add_profiler_path(profiler_path)
|
|
197
|
+
import profiler
|
|
198
|
+
bin_counter = 1
|
|
199
|
+
### read lshforest
|
|
200
|
+
p = profiler.Profiler(lshforestpath = lshforestfile,
|
|
201
|
+
hashes_h5= hashes_h5,
|
|
202
|
+
#mat_path= fam2orthoxmlfile ,
|
|
203
|
+
oma = False ,
|
|
204
|
+
nsamples = 256 ,
|
|
205
|
+
mastertree = treefile,
|
|
206
|
+
slicesubhogs = True
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
### ensure Profiler has fam_dict
|
|
210
|
+
if not hasattr(p, 'fam_dict') or p.fam_dict is None:
|
|
211
|
+
print("Error: fam_dict is None.")
|
|
212
|
+
print(p.__dict__.keys())
|
|
213
|
+
exit(1)
|
|
214
|
+
|
|
215
|
+
### iterate over every bin large enough
|
|
216
|
+
for species_bin, group_df in samples_info_df.groupby(bin_name):
|
|
217
|
+
binname = species_bin
|
|
218
|
+
bin_counter+=1
|
|
219
|
+
bin_dict[binname] = species_bin
|
|
220
|
+
if os.path.exists(os.path.join(outputdir, f"allvsall_hashmat_{binname}.npy")):
|
|
221
|
+
continue
|
|
222
|
+
### get group_df with subhog ids in this bin from subhogs_sampled_pairs_df
|
|
223
|
+
subhogs_in_bin = set(group_df['subhog'])
|
|
224
|
+
# Filter pairs where both subhogs are in this bin
|
|
225
|
+
bin_pairs_df = subhogs_sampled_pairs_df[
|
|
226
|
+
subhogs_sampled_pairs_df['subhog'].isin(subhogs_in_bin)
|
|
227
|
+
]
|
|
228
|
+
if len(bin_pairs_df) < 10:
|
|
229
|
+
continue
|
|
230
|
+
print(f"Processing bin: {species_bin}")
|
|
231
|
+
### replacing get_hog_to_hog_jaccards
|
|
232
|
+
jaccard_results = []
|
|
233
|
+
### split bin_pairs_df in half to make comparisons
|
|
234
|
+
bin_paired_for_comparison_df = turn_subhogs_to_pairs(bin_pairs_df)
|
|
235
|
+
|
|
236
|
+
for index, row in bin_paired_for_comparison_df.iterrows():
|
|
237
|
+
subhog1 = row['subhog1']
|
|
238
|
+
subhog2 = row['subhog2']
|
|
239
|
+
try:
|
|
240
|
+
jaccard_value = p.hog_v_hog([subhog1, subhog2])
|
|
241
|
+
jaccard_results.append({
|
|
242
|
+
'subhog1': subhog1,
|
|
243
|
+
'subhog2': subhog2,
|
|
244
|
+
'jaccard': jaccard_value
|
|
245
|
+
})
|
|
246
|
+
except Exception as e:
|
|
247
|
+
print(f"Error calculating Jaccard for {subhog1} and {subhog2}: {e}")
|
|
248
|
+
jaccard_results.append({
|
|
249
|
+
'subhog1': subhog1,
|
|
250
|
+
'subhog2': subhog2,
|
|
251
|
+
'jaccard': None
|
|
252
|
+
})
|
|
253
|
+
# Convert results to DataFrame
|
|
254
|
+
jaccard_df = pd.DataFrame(jaccard_results)
|
|
255
|
+
# Save to CSV
|
|
256
|
+
jaccard_df.to_csv(os.path.join(outputdir, f"sampled_jaccard_results_{binname}.csv"), index=False)
|
|
257
|
+
print(f"Jaccard results saved to {os.path.join(outputdir, f'sampled_jaccard_results_{binname}.csv')}\n")
|
|
258
|
+
|
|
259
|
+
def turn_subhogs_to_pairs(subhogs_sampled_pairs_df):
|
|
260
|
+
# Shuffle the DataFrame to ensure randomness
|
|
261
|
+
shuffled_df = subhogs_sampled_pairs_df.sample(frac=1, random_state=42).reset_index(drop=True)
|
|
262
|
+
# Create pairs by taking two consecutive rows
|
|
263
|
+
pairs_list = []
|
|
264
|
+
for i in range(0, len(shuffled_df) - 1, 2):
|
|
265
|
+
pairs_list.append({
|
|
266
|
+
'subhog1': shuffled_df.iloc[i]['subhog'],
|
|
267
|
+
'subhog2': shuffled_df.iloc[i + 1]['subhog']
|
|
268
|
+
})
|
|
269
|
+
# Convert the list of pairs to a DataFrame
|
|
270
|
+
pairs_df = pd.DataFrame(pairs_list)
|
|
271
|
+
return pairs_df
|
|
272
|
+
|
|
273
|
+
def taxid_to_name(taxid, expected_root="Metazoa"):
|
|
274
|
+
if taxid == 'internal_0':
|
|
275
|
+
print(f"Expected root: {expected_root}")
|
|
276
|
+
return f'root_{expected_root}'
|
|
277
|
+
try:
|
|
278
|
+
taxid = int(taxid)
|
|
279
|
+
### NOTE: maybe this exception should be handled differently
|
|
280
|
+
# for now just return empty string
|
|
281
|
+
except (ValueError, TypeError):
|
|
282
|
+
print(f"Invalid taxid: {taxid}\nUsing ''.")
|
|
283
|
+
return ''
|
|
284
|
+
name_dict = ncbi.get_taxid_translator([taxid])#.translate_to_names([taxid])
|
|
285
|
+
#print(name_dict)
|
|
286
|
+
if len(name_dict) > 1:
|
|
287
|
+
print(f"Warning: More than one name found for taxid {taxid}. Using the first one.")
|
|
288
|
+
#print(name_dict)
|
|
289
|
+
return name_dict[taxid]
|
|
290
|
+
|
|
291
|
+
def main(hogprofoutputfolder, outputdir, profiler_path, expected_root):
|
|
292
|
+
|
|
293
|
+
### get all variables:
|
|
294
|
+
fam2orthoxmlfile = os.path.join(hogprofoutputfolder, "fam2orthoxml.csv")
|
|
295
|
+
hashes_h5 = os.path.join(hogprofoutputfolder, "hashes.h5")
|
|
296
|
+
lshforestfile = os.path.join(hogprofoutputfolder, "newlshforest.pkl")
|
|
297
|
+
treefile = os.path.join(hogprofoutputfolder, "reformatted_tree.nwk")
|
|
298
|
+
hogmetadatafile = os.path.join(hogprofoutputfolder, "subhogize_table.csv")
|
|
299
|
+
taxaindexfile = os.path.join(hogprofoutputfolder, "idmapper.pkl")
|
|
300
|
+
|
|
301
|
+
### print input parameters
|
|
302
|
+
print(f"fam2orthoxmlfile: {fam2orthoxmlfile}")
|
|
303
|
+
print(f"hogmetadatafile: {hogmetadatafile}")
|
|
304
|
+
print(f"lshforestfile: {lshforestfile}")
|
|
305
|
+
print(f"hashes_h5: {hashes_h5}")
|
|
306
|
+
print(f"treefile: {treefile}")
|
|
307
|
+
print(f"taxaindexfile: {taxaindexfile}")
|
|
308
|
+
print(f"outputdir: {outputdir}\n")
|
|
309
|
+
|
|
310
|
+
#print("\nWARNING: This script only works locally for now. Do not try on curnagl!\n")
|
|
311
|
+
print("Reminder: if this script does not run on curnagl, you need to reinstall in editable mode (pip install -e .)")
|
|
312
|
+
|
|
313
|
+
### create output directory if it does not exist
|
|
314
|
+
outputdir = create_directory(outputdir)
|
|
315
|
+
|
|
316
|
+
### get taxidmapper:
|
|
317
|
+
with open(taxaindexfile, 'rb') as f:
|
|
318
|
+
taxidmapper = pickle.load(f)
|
|
319
|
+
|
|
320
|
+
### get subhogids from fam2orthoxml file
|
|
321
|
+
subhogids_df = pd.read_csv(fam2orthoxmlfile)
|
|
322
|
+
### if it is an OmaServer run it will not have column labels, so add them (fam, subhog_id)
|
|
323
|
+
if 'fam' not in subhogids_df.columns:
|
|
324
|
+
subhogids_df.columns = ['fam', 'subhog_id'] + list(subhogids_df.columns[2:])
|
|
325
|
+
### merge first column with subhog_id column
|
|
326
|
+
subhogids_df['subhogid_full'] = subhogids_df['fam'].astype(str) + "_" + subhogids_df['subhog_id'].astype(str)
|
|
327
|
+
### else it is an OmaServer run with no column labels
|
|
328
|
+
### use as index
|
|
329
|
+
subhogids_df.set_index('subhogid_full', inplace=True)
|
|
330
|
+
### add taxid column (first part of subhog_id)
|
|
331
|
+
subhogids_df['taxid'] = subhogids_df['subhog_id'].astype(str).apply(lambda x: x.split('_')[0])
|
|
332
|
+
|
|
333
|
+
### create bins and sample subhogs
|
|
334
|
+
if not os.path.exists(os.path.join(outputdir, "subhogs_sampled_pairs.csv")):
|
|
335
|
+
idmapper = create_bins(subhogids_df, outputdir, taxidmapper, samplesize=500) ### used to be hogmetadata_df ###500 HOGs, later 250 pairs to create distribution
|
|
336
|
+
else:
|
|
337
|
+
idmapper = {v: k for k, v in taxidmapper.items()}
|
|
338
|
+
|
|
339
|
+
### read samples as pairs
|
|
340
|
+
subhogs_sampled_pairs_df = pd.read_csv(os.path.join(outputdir, "subhogs_sampled_pairs.csv"), index_col=None)
|
|
341
|
+
print(f"Reading sampled pairs")
|
|
342
|
+
print(subhogs_sampled_pairs_df.head())
|
|
343
|
+
|
|
344
|
+
### get bin specific all vs all
|
|
345
|
+
samples_info_df = pd.read_csv(os.path.join(outputdir, "subhogs_samples_info.csv"), index_col=0)
|
|
346
|
+
|
|
347
|
+
bin_dict = {}
|
|
348
|
+
bin_name = 'taxid' #'species_bin'
|
|
349
|
+
### generate jaccard similarities for each bin
|
|
350
|
+
get_bins_jaccards_pairwise(samples_info_df, bin_name, bin_dict, subhogs_sampled_pairs_df, lshforestfile, hashes_h5, treefile, outputdir, profiler_path)
|
|
351
|
+
|
|
352
|
+
bin_dict = {}
|
|
353
|
+
bin_name = 'taxid' #'species_bin'
|
|
354
|
+
print(f"Number of unique bins in '{bin_name}': {samples_info_df[bin_name].nunique()}")
|
|
355
|
+
|
|
356
|
+
bin_counter = 1
|
|
357
|
+
### iterate over every bin large enough
|
|
358
|
+
for species_bin, group_df in samples_info_df.groupby(bin_name):
|
|
359
|
+
binname = species_bin
|
|
360
|
+
bin_counter+=1
|
|
361
|
+
bin_dict[binname] = species_bin
|
|
362
|
+
if os.path.exists(os.path.join(outputdir, f"allvsall_hashmat_{binname}.npy")):
|
|
363
|
+
continue
|
|
364
|
+
### get group_df with subhog ids in this bin from subhogs_sampled_pairs_df
|
|
365
|
+
subhogs_in_bin = set(group_df['subhog'])
|
|
366
|
+
# Filter pairs where both subhogs are in this bin
|
|
367
|
+
bin_pairs_df = subhogs_sampled_pairs_df[
|
|
368
|
+
subhogs_sampled_pairs_df['subhog'].isin(subhogs_in_bin)
|
|
369
|
+
]
|
|
370
|
+
if bin_pairs_df.shape[0] < 10:
|
|
371
|
+
continue
|
|
372
|
+
print(f"Processing bin: {species_bin}")
|
|
373
|
+
allvsall_hashmat = get_hog_to_hog_jaccards(bin_pairs_df, lshforestfile, hashes_h5, treefile,
|
|
374
|
+
fam2orthoxmlfile, outputdir, profiler_path, allvsall=True, suffix=binname)
|
|
375
|
+
|
|
376
|
+
### plot all vs all jaccard distribution
|
|
377
|
+
plt.figure(figsize=(8, 5))
|
|
378
|
+
plt.hist(allvsall_hashmat.flatten(), bins=50, color='purple', alpha=0.7)
|
|
379
|
+
plt.xlabel('Jaccard Similarity')
|
|
380
|
+
plt.ylabel('Frequency')
|
|
381
|
+
plt.title(f'All vs All Jaccard Similarity Distribution {species_bin}')
|
|
382
|
+
plt.tight_layout()
|
|
383
|
+
plt.savefig(os.path.join(outputdir, f"allvsall_jaccard_distribution_{binname}.png"))
|
|
384
|
+
|
|
385
|
+
if not os.path.exists(os.path.join(outputdir, f"bins_jaccard_thresholds.csv")):
|
|
386
|
+
#### separately deal with each bin
|
|
387
|
+
allvsall_files = sorted(glob.glob(os.path.join(outputdir, "allvsall_hashmat_*.npy")))
|
|
388
|
+
print(f"Found {len(allvsall_files)} bin-specific all-vs-all files:")
|
|
389
|
+
bins_dicts = []
|
|
390
|
+
for allvsallfile in allvsall_files:
|
|
391
|
+
if 'bin' in os.path.basename(allvsallfile):
|
|
392
|
+
continue
|
|
393
|
+
if not os.path.exists(os.path.join(outputdir, f"bins_jaccard_thresholds.csv")):
|
|
394
|
+
binname = os.path.basename(allvsallfile).split("allvsall_hashmat_")[1][:-4]
|
|
395
|
+
if binname == '':
|
|
396
|
+
continue
|
|
397
|
+
binnum = int(binname.split("bin")[-1])
|
|
398
|
+
### read all vs all hashmat
|
|
399
|
+
allvsall_hashmat = np.load(allvsallfile)
|
|
400
|
+
# Remove self hits (diagonal)
|
|
401
|
+
if allvsall_hashmat.ndim == 2:
|
|
402
|
+
allvsall_flat = allvsall_hashmat[~np.eye(allvsall_hashmat.shape[0], dtype=bool)].flatten()
|
|
403
|
+
else:
|
|
404
|
+
# Already 1D, nothing to mask
|
|
405
|
+
allvsall_flat = allvsall_hashmat
|
|
406
|
+
### calculate threshold for bin
|
|
407
|
+
empirical_t = get_jaccard_threshold(allvsall_flat, alpha=0.01)
|
|
408
|
+
bins_dicts.append({
|
|
409
|
+
'bin': binname,
|
|
410
|
+
'bin_range': bin_dict[binnum],
|
|
411
|
+
'empirical_t': empirical_t,
|
|
412
|
+
'sampled_comparisons': len(allvsall_flat)
|
|
413
|
+
})
|
|
414
|
+
### turn to df
|
|
415
|
+
# Convert results to DataFrame
|
|
416
|
+
bins_thresholds_df = pd.DataFrame(bins_dicts)
|
|
417
|
+
print(bins_thresholds_df.head())
|
|
418
|
+
### here add idmapper to have taxnames together with the thresholds
|
|
419
|
+
# Assumes taxids were used in the species tree and taxnames in the orthoxml file.
|
|
420
|
+
# This is not always the case.
|
|
421
|
+
bins_thresholds_df['taxid'] = bins_thresholds_df['bin'].map(idmapper)
|
|
422
|
+
### here add actual taxon names
|
|
423
|
+
bins_thresholds_df['taxname'] = bins_thresholds_df['taxid'].apply(taxid_to_name, expected_root=expected_root)
|
|
424
|
+
### check here if the assumption was correct - if the column is full of ''
|
|
425
|
+
if bins_thresholds_df['taxname'].isnull().all() or (bins_thresholds_df['taxname'] == '').all():
|
|
426
|
+
# remove that column
|
|
427
|
+
bins_thresholds_df.drop(columns=['taxname'], inplace=True)
|
|
428
|
+
# rename taxid column to taxname
|
|
429
|
+
bins_thresholds_df.rename(columns={'taxid': 'taxname'}, inplace=True)
|
|
430
|
+
# replace NaN with root if bin is 0
|
|
431
|
+
bins_thresholds_df.loc[
|
|
432
|
+
(bins_thresholds_df['bin'].astype(int) == 0) &
|
|
433
|
+
(bins_thresholds_df['taxname'].isna()),
|
|
434
|
+
'taxname'
|
|
435
|
+
] = f'root_{expected_root}'
|
|
436
|
+
# Save to CSV
|
|
437
|
+
bins_thresholds_df.to_csv(os.path.join(outputdir, f"bins_jaccard_thresholds.csv"), index=False)
|
|
438
|
+
print(f"Bins thresholds saved to {os.path.join(outputdir, f'bins_jaccard_thresholds.csv')}\n")
|
|
439
|
+
|
|
440
|
+
### read bins thresholds df
|
|
441
|
+
bins_thresholds_df = pd.read_csv(os.path.join(outputdir, f"bins_jaccard_thresholds.csv"), index_col=None)
|
|
442
|
+
print(bins_thresholds_df.head())
|
|
443
|
+
|
|
444
|
+
### remove bins that cannot be trusted - setting min sample size based on chatGPT suggestion
|
|
445
|
+
bins_thresholds_df = bins_thresholds_df[bins_thresholds_df['sampled_comparisons']> 150]
|
|
446
|
+
|
|
447
|
+
# Convert bin_range to string for categorical plotting
|
|
448
|
+
bins_thresholds_df['bin_range_str'] = bins_thresholds_df['bin_range'].astype(str)
|
|
449
|
+
bins_thresholds_df.sort_values(by='bin_range', inplace=True)
|
|
450
|
+
|
|
451
|
+
### plot bin max to thresholds
|
|
452
|
+
plt.figure(figsize=(max(8, 0.5 * len(bins_thresholds_df)), 5)) # dynamic width
|
|
453
|
+
x = np.arange(len(bins_thresholds_df))
|
|
454
|
+
plt.scatter(x, bins_thresholds_df['empirical_t'], marker='o', label='Empirical threshold')
|
|
455
|
+
plt.xlabel('Bin')
|
|
456
|
+
### rotate x labels
|
|
457
|
+
plt.xticks(x, bins_thresholds_df['taxname'], rotation=45, ha='right')
|
|
458
|
+
plt.ylabel('Jaccard threshold')
|
|
459
|
+
plt.title('Jaccard Thresholds vs. Bin')
|
|
460
|
+
plt.legend()
|
|
461
|
+
plt.tight_layout(rect=[0, 0.05, 1, 1]) # leave room at bottom
|
|
462
|
+
plt.subplots_adjust(bottom=0.35) # guarantee label visibility
|
|
463
|
+
plt.savefig(os.path.join(outputdir, "bin_vs_jaccard_thresholds.png"),dpi=300, bbox_inches='tight')
|
|
464
|
+
plt.close()
|
|
465
|
+
|
|
466
|
+
def parse_args():
|
|
467
|
+
default_profiler = os.path.join(
|
|
468
|
+
os.path.dirname(os.path.abspath(__file__)),
|
|
469
|
+
"profiler.py",
|
|
470
|
+
)
|
|
471
|
+
parser = argparse.ArgumentParser(
|
|
472
|
+
description="Generate jaccard distributions for theshold testing. Designed for HogProf levels."
|
|
473
|
+
)
|
|
474
|
+
parser.add_argument(
|
|
475
|
+
"--hogprof_folder",
|
|
476
|
+
required=True,
|
|
477
|
+
help="Folder where the result of HogProf LSHbuilder is stored."
|
|
478
|
+
)
|
|
479
|
+
parser.add_argument(
|
|
480
|
+
"--expected_root",
|
|
481
|
+
required=False,
|
|
482
|
+
default="Metazoa",
|
|
483
|
+
help="Expected root taxon (to replace 'internal_0')."
|
|
484
|
+
)
|
|
485
|
+
parser.add_argument(
|
|
486
|
+
"--output",
|
|
487
|
+
required=True,
|
|
488
|
+
help="Output folder."
|
|
489
|
+
)
|
|
490
|
+
parser.add_argument(
|
|
491
|
+
"--profiler-path",
|
|
492
|
+
default=default_profiler,
|
|
493
|
+
help=(
|
|
494
|
+
"Path to profiler.py. "
|
|
495
|
+
"Defaults to profiler.py in the same directory as this script."
|
|
496
|
+
),
|
|
497
|
+
)
|
|
498
|
+
return parser.parse_args()
|
|
499
|
+
|
|
500
|
+
if __name__ == '__main__':
|
|
501
|
+
args = parse_args()
|
|
502
|
+
main(
|
|
503
|
+
args.hogprof_folder,
|
|
504
|
+
args.output,
|
|
505
|
+
args.profiler_path,
|
|
506
|
+
args.expected_root
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
### print end time
|
|
510
|
+
end_time_secs = time()
|
|
511
|
+
end_time = datetime.fromtimestamp(end_time_secs).strftime('%Y-%m-%d %H:%M:%S')
|
|
512
|
+
print(f"Script finished at: {end_time}\n")
|
|
513
|
+
execution_time = end_time_secs - start_time_secs
|
|
514
|
+
### print execution time but reformat it to be human readable
|
|
515
|
+
print(f"Total execution time: {round(execution_time, 2)} seconds\n")
|
|
516
|
+
print("\nDONE!\n")
|