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.
@@ -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")