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,518 @@
|
|
|
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
|
+
from time import time
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
import numpy as np
|
|
10
|
+
import seaborn as sns
|
|
11
|
+
import re
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
import csv
|
|
14
|
+
from tqdm import tqdm
|
|
15
|
+
from scipy.spatial.distance import squareform
|
|
16
|
+
from scipy.cluster.hierarchy import linkage
|
|
17
|
+
# test to see if this fixes heatmap issue
|
|
18
|
+
sys.setrecursionlimit(10000)
|
|
19
|
+
from ete3 import NCBITaxa, Tree
|
|
20
|
+
ncbi = NCBITaxa()
|
|
21
|
+
|
|
22
|
+
### get current time
|
|
23
|
+
start_time_secs = time()
|
|
24
|
+
start_time = datetime.fromtimestamp(start_time_secs).strftime('%Y-%m-%d %H:%M:%S')
|
|
25
|
+
### print start time human readable
|
|
26
|
+
print(f"Script started at: {start_time}\n")
|
|
27
|
+
|
|
28
|
+
def add_profiler_path(profiler_path):
|
|
29
|
+
"""Add the directory containing profiler.py to Python's sys.path"""
|
|
30
|
+
if not os.path.exists(profiler_path):
|
|
31
|
+
raise FileNotFoundError(f"Profiler path does not exist: {profiler_path}")
|
|
32
|
+
profiler_dir = os.path.dirname(os.path.abspath(profiler_path))
|
|
33
|
+
if profiler_dir in sys.path:
|
|
34
|
+
sys.path.remove(profiler_dir)
|
|
35
|
+
sys.path.insert(0, profiler_dir)
|
|
36
|
+
|
|
37
|
+
def create_directory(dirpath):
|
|
38
|
+
if not os.path.exists(dirpath):
|
|
39
|
+
os.mkdir(dirpath)
|
|
40
|
+
return dirpath
|
|
41
|
+
|
|
42
|
+
'''https://stackoverflow.com/questions/46135839/auto-detect-the-delimiter-in-a-csv-file-using-pd-read-csv'''
|
|
43
|
+
def get_delimiter(file_path, bytes = 4096):
|
|
44
|
+
sniffer = csv.Sniffer()
|
|
45
|
+
data = open(file_path, "r").read(bytes)
|
|
46
|
+
delimiter = sniffer.sniff(data).delimiter
|
|
47
|
+
return delimiter
|
|
48
|
+
|
|
49
|
+
def get_hashids(subhogs_table, fam2orthoxml_file):
|
|
50
|
+
sep = get_delimiter(subhogs_table)
|
|
51
|
+
subhogs_df = pd.read_csv(subhogs_table, sep=sep)
|
|
52
|
+
if 'hashid' in subhogs_df.columns:
|
|
53
|
+
return subhogs_table
|
|
54
|
+
print(f"Extracting hashids from file: {fam2orthoxml_file}")
|
|
55
|
+
subhogid_col = "subhogid"
|
|
56
|
+
if subhogid_col not in subhogs_df.columns:
|
|
57
|
+
raise ValueError(f"Column '{subhogid_col}' not found in {subhogs_table}. Please ensure the input file has a column named '{subhogid_col}'.")
|
|
58
|
+
subhogs_df.set_index(subhogid_col, inplace=True)
|
|
59
|
+
### read in the fam2orthoxml table generated by HogProf lshbuilder and the tree file
|
|
60
|
+
fam2orthoxml_df = pd.read_csv(fam2orthoxml_file)
|
|
61
|
+
# build mapping: subhog_id -> hash row index
|
|
62
|
+
subhog_to_hashid = {subhog: idx for idx, subhog in enumerate(fam2orthoxml_df['subhog_id'])}
|
|
63
|
+
# attach hash ids
|
|
64
|
+
subhogs_df['hashid'] = subhogs_df.index.map(subhog_to_hashid)
|
|
65
|
+
# outputqueryfile
|
|
66
|
+
outputqueryfile = subhogs_table.replace(f"{os.path.splitext(subhogs_table)[1]}",
|
|
67
|
+
f"_queries{os.path.splitext(subhogs_table)[1]}")
|
|
68
|
+
# save new file with hash ids
|
|
69
|
+
subhogs_df.to_csv(outputqueryfile, sep=sep)
|
|
70
|
+
return outputqueryfile
|
|
71
|
+
|
|
72
|
+
def get_hashids_by_subhogid(subhog_ids, fam2orthoxml_file):
|
|
73
|
+
fam2orthoxml_df = pd.read_csv(fam2orthoxml_file)
|
|
74
|
+
subhog_to_hashid = {subhog: idx for idx, subhog in enumerate(fam2orthoxml_df['subhog_id'])}
|
|
75
|
+
hashids = [subhog_to_hashid[subhog] for subhog in subhog_ids if subhog in subhog_to_hashid]
|
|
76
|
+
return hashids
|
|
77
|
+
|
|
78
|
+
'''function to add some info to the extracted hits about how the origin and strength of coevolution'''
|
|
79
|
+
def find_coevolution_origin(hits_df, treepath):
|
|
80
|
+
print("Finding coevolution origins...")
|
|
81
|
+
tree = Tree(treepath, format=1)
|
|
82
|
+
node_lookup = {int(n.name): n for n in tree.traverse() if n.name}
|
|
83
|
+
def edge_to_pair(row):
|
|
84
|
+
return frozenset({
|
|
85
|
+
re.search(r'_(HOG:[^_.]+)', row["query_hog"]).group(1),
|
|
86
|
+
re.search(r'_(HOG:[^_.]+)', row["target_hog"]).group(1)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
@lru_cache(maxsize=None)
|
|
90
|
+
def lineage(taxid):
|
|
91
|
+
node = node_lookup.get(int(taxid))
|
|
92
|
+
#print("taxid:", taxid, "node:", node)
|
|
93
|
+
if node is None:
|
|
94
|
+
return frozenset()
|
|
95
|
+
return frozenset(int(n.name) for n in node.get_ancestors()) | {int(taxid)}
|
|
96
|
+
|
|
97
|
+
def highest_non_overlapping_taxa(taxids):
|
|
98
|
+
taxids = sorted(set(map(int, taxids)))
|
|
99
|
+
keep = []
|
|
100
|
+
for t in taxids:
|
|
101
|
+
# Remove t if any other observed taxon is an ancestor of it
|
|
102
|
+
if not any(
|
|
103
|
+
other != t and other in lineage(t)
|
|
104
|
+
for other in taxids):
|
|
105
|
+
keep.append(t)
|
|
106
|
+
return keep
|
|
107
|
+
# 1 add column for checking
|
|
108
|
+
hits_df['pair'] = hits_df.apply(edge_to_pair, axis=1)
|
|
109
|
+
# 2 group by pair so that each pair has a list of taxa it appears in
|
|
110
|
+
hits_grouped_df = hits_df.groupby('pair')['taxid'].apply(list).reset_index(name='taxid_list')
|
|
111
|
+
# add column with number of taxids per pair
|
|
112
|
+
hits_grouped_df['families_score'] = hits_grouped_df['taxid_list'].apply(len)
|
|
113
|
+
# 3 find highest rank taxon (or taxa) for each list
|
|
114
|
+
hits_grouped_df["origin_taxids"] = hits_grouped_df["taxid_list"].apply(highest_non_overlapping_taxa)
|
|
115
|
+
# 4 add to hits_df column '1st_occurence' with True for the rows that have a taxid in origin_taxids and False otherwise
|
|
116
|
+
hits_df = hits_df.merge(hits_grouped_df[['pair', 'families_score','origin_taxids']], on='pair', how='left')
|
|
117
|
+
hits_df['1st_occurence'] = hits_df.apply(lambda row: row['taxid'] in row['origin_taxids'], axis=1)
|
|
118
|
+
# 5 remove 'pair' column and 'origin_taxids' column
|
|
119
|
+
hits_df.drop(columns=['pair', 'origin_taxids'], inplace=True)
|
|
120
|
+
return hits_df
|
|
121
|
+
|
|
122
|
+
'''use hashes directly to extract hits'''
|
|
123
|
+
def extract_hits_from_hashes(lshforestpath, hashes_h5, treepath, outputfile, profiler_path,
|
|
124
|
+
allvsall, k, subhogs_table, thresholds_df, treeroot=""):
|
|
125
|
+
# Add profiler directory to path and import
|
|
126
|
+
add_profiler_path(profiler_path)
|
|
127
|
+
import profiler
|
|
128
|
+
print("Imported profiler from:")
|
|
129
|
+
print(profiler.__file__)
|
|
130
|
+
subhogs_df = pd.read_csv(subhogs_table, sep=get_delimiter(subhogs_table), index_col=0)
|
|
131
|
+
## index label is subhogid
|
|
132
|
+
subhogs_df.index.name = 'subhogid'
|
|
133
|
+
print(subhogs_df.head())
|
|
134
|
+
subhog_to_hashid = subhogs_df['hashid'].to_dict()
|
|
135
|
+
### check that hashid were indeed extracted
|
|
136
|
+
if any(pd.isnull(list(subhog_to_hashid.values()))):
|
|
137
|
+
print("subhog_to_hashid:",subhog_to_hashid)
|
|
138
|
+
raise ValueError("Some subHOGs do not have hashid. Please check the queries file.")
|
|
139
|
+
hashid_to_subhog = {v: k for k, v in subhog_to_hashid.items()}
|
|
140
|
+
print("\nBuilding the profiler")
|
|
141
|
+
p = profiler.Profiler(lshforestpath = lshforestpath,
|
|
142
|
+
hashes_h5= hashes_h5,
|
|
143
|
+
oma = False ,
|
|
144
|
+
nsamples = 256 ,
|
|
145
|
+
mastertree = treepath,
|
|
146
|
+
slicesubhogs = True
|
|
147
|
+
)
|
|
148
|
+
p.slicesubhogs = False ### setting as False to use original hogid2fam function in profiler.py
|
|
149
|
+
# the reason for this is using family IDS vs hashids for querying. the original function used hashids,
|
|
150
|
+
# sometimes in levels we want to query everything within a family though.
|
|
151
|
+
print("Pulling hashes...")
|
|
152
|
+
raw_profiles = p.pull_hashes(subhog_to_hashid.values()) # returns hashid to hashsig dict
|
|
153
|
+
### here check that the expected number of hashes was pulled
|
|
154
|
+
if len(raw_profiles)!= len(hashid_to_subhog):
|
|
155
|
+
print("Warning! Could not pull all the hashes!")
|
|
156
|
+
subhogs_without_hash = []
|
|
157
|
+
for subhog, hashid in subhog_to_hashid.items():
|
|
158
|
+
if hashid not in raw_profiles.keys():
|
|
159
|
+
subhogs_without_hash.append(subhog)
|
|
160
|
+
if len(subhogs_without_hash)>0:
|
|
161
|
+
print("Examples:", subhogs_without_hash[:5])
|
|
162
|
+
profiles = {
|
|
163
|
+
hashid_to_subhog[h]: {
|
|
164
|
+
'hashsig': raw_profiles[h],
|
|
165
|
+
'hashid': h
|
|
166
|
+
}
|
|
167
|
+
for h in raw_profiles
|
|
168
|
+
if h in hashid_to_subhog
|
|
169
|
+
}
|
|
170
|
+
profiles_df = pd.DataFrame.from_dict(profiles, orient='index')
|
|
171
|
+
profiles_num = len(profiles_df.index.unique())
|
|
172
|
+
### checking for empty / degenerate /invalid signatures
|
|
173
|
+
profiles_df['sigsum'] = profiles_df.hashsig.map(lambda x : np.sum(x.digest()))
|
|
174
|
+
profiles_df = profiles_df[profiles_df.sigsum>0]
|
|
175
|
+
#print(f'{len(profiles.index.unique())} HOGs have valid signatures')
|
|
176
|
+
if profiles_num != len(profiles_df.index.unique()):
|
|
177
|
+
print("Warning: invalid hash signatures detected!")
|
|
178
|
+
|
|
179
|
+
def get_candidates(p, query_sig, k):
|
|
180
|
+
# query LSH forest for candidate hash ids - get top k
|
|
181
|
+
candidate_subhogids = [target_hog[target_hog.find('_')+1:] for target_hog in p.forest.query(query_sig, k)]
|
|
182
|
+
candidate_hashids = [p.subhog_to_hashids_dict[target_hog] for target_hog in candidate_subhogids]
|
|
183
|
+
local_hashid_to_subhog = {int(h): s for h, s in zip(candidate_hashids, candidate_subhogids)}
|
|
184
|
+
# pull candidate signatures (dict: hashid -> MinHash)
|
|
185
|
+
candidate_hashes = p.pull_hashes(candidate_hashids)
|
|
186
|
+
return candidate_hashids, local_hashid_to_subhog, candidate_hashes
|
|
187
|
+
|
|
188
|
+
def get_scores(candidate_hashids,candidate_hashes, query_sig, level_threshold):
|
|
189
|
+
scores = {}
|
|
190
|
+
for hashid in candidate_hashids: ### candidate_hashids are the closest but they are not ordered
|
|
191
|
+
sig = candidate_hashes[hashid]
|
|
192
|
+
this_score = sig.jaccard(query_sig)
|
|
193
|
+
if this_score >= level_threshold:
|
|
194
|
+
scores[hashid] = this_score
|
|
195
|
+
return scores
|
|
196
|
+
|
|
197
|
+
### get jaccard scores too
|
|
198
|
+
rows = []
|
|
199
|
+
counter = 0
|
|
200
|
+
no_hits_subhogs = []
|
|
201
|
+
### data structure_for all_vs_all
|
|
202
|
+
taxa_networks_dict = {}
|
|
203
|
+
network_id_to_hid = {}
|
|
204
|
+
level_to_T_dict = {}
|
|
205
|
+
taxid_to_taxname_dict = thresholds_df['taxname'].to_dict()
|
|
206
|
+
print("Querying each HOG...")
|
|
207
|
+
for query_subhog_id, querydict in profiles.items():
|
|
208
|
+
### clean hogids e.g. 14_HOG:0004777_537 where 14 is the internal taxid (bin in thresholds)
|
|
209
|
+
# and 537 is the internal hog id from the orthoxml transversal
|
|
210
|
+
query_level = int(query_subhog_id.split("_")[0])
|
|
211
|
+
hog_id_str = query_subhog_id.split("_")[1]
|
|
212
|
+
try:
|
|
213
|
+
level_threshold = thresholds_df['empirical_t'].loc[query_level]
|
|
214
|
+
except:
|
|
215
|
+
level_threshold = 0 ### take everything if no T is specified
|
|
216
|
+
print(f"Warning: no T defined for {query_level}")
|
|
217
|
+
level_to_T_dict[query_level] = level_threshold
|
|
218
|
+
current_taxon = thresholds_df['taxname'].loc[query_level]
|
|
219
|
+
query_sig = querydict['hashsig']
|
|
220
|
+
query_hashid = querydict['hashid']
|
|
221
|
+
# query LSH forest for candidate hash ids - get top k
|
|
222
|
+
candidate_hashids, local_hashid_to_subhog, candidate_hashes = get_candidates(p, query_sig, k)
|
|
223
|
+
scores = get_scores(candidate_hashids,candidate_hashes, query_sig, level_threshold)
|
|
224
|
+
### it is possible that none pass the threshold
|
|
225
|
+
if len(scores) == 0:
|
|
226
|
+
no_hits_subhogs.append(query_subhog_id)
|
|
227
|
+
continue
|
|
228
|
+
### it is also possible that all pass the threshold, so we need to check more (only check once more with double k)
|
|
229
|
+
if len(scores) == len(candidate_hashids):
|
|
230
|
+
candidate_hashids, local_hashid_to_subhog, candidate_hashes = get_candidates(p, query_sig, 2*k)
|
|
231
|
+
scores = get_scores(candidate_hashids,candidate_hashes, query_sig, level_threshold)
|
|
232
|
+
# sort candidate scores descending
|
|
233
|
+
scored_pairs = sorted(scores.items(), key=lambda x: x[1], reverse=True)
|
|
234
|
+
'''
|
|
235
|
+
# save info for allvsall
|
|
236
|
+
if allvsall:
|
|
237
|
+
if query_level not in taxa_networks_dict.keys():
|
|
238
|
+
network_id_to_hid = {}
|
|
239
|
+
else:
|
|
240
|
+
network_id_to_hid = taxa_networks_dict[query_level]
|
|
241
|
+
# add new info
|
|
242
|
+
network_id_to_hid[query_subhog_id] = query_hashid
|
|
243
|
+
taxa_networks_dict[query_level] = network_id_to_hid
|
|
244
|
+
'''
|
|
245
|
+
# build rows: one row per query-target pair
|
|
246
|
+
for hid, jaccard in scored_pairs:
|
|
247
|
+
target_subhog = local_hashid_to_subhog[hid]
|
|
248
|
+
|
|
249
|
+
# target_subhog expected like "14_HOG:0004777_537" or "14_751313"
|
|
250
|
+
target_hog_clean = target_subhog # already in desired form
|
|
251
|
+
try:
|
|
252
|
+
target_level = int(target_hog_clean.split("_")[0])
|
|
253
|
+
except Exception:
|
|
254
|
+
# if parsing fails, skip
|
|
255
|
+
print(f"Warning: cannot parse level from {target_hog_clean}; skipping")
|
|
256
|
+
continue
|
|
257
|
+
try:
|
|
258
|
+
target_hog_id_str = target_hog_clean.split("_")[1]
|
|
259
|
+
except Exception:
|
|
260
|
+
target_hog_id_str = ""
|
|
261
|
+
# don't keep self hits:
|
|
262
|
+
if query_subhog_id ==target_hog_clean:
|
|
263
|
+
continue
|
|
264
|
+
# only keep same-level hits so threshold makes sense
|
|
265
|
+
if target_level != query_level:
|
|
266
|
+
continue
|
|
267
|
+
# flag same-family (compare the id string parts)
|
|
268
|
+
same_fam = (hog_id_str == target_hog_id_str)
|
|
269
|
+
# append row
|
|
270
|
+
rows.append({
|
|
271
|
+
'query_hog': query_subhog_id,
|
|
272
|
+
'target_hog': target_hog_clean,
|
|
273
|
+
'jaccard': jaccard,
|
|
274
|
+
'empirical_t': level_threshold,
|
|
275
|
+
'taxname': current_taxon,
|
|
276
|
+
'taxid': query_level,
|
|
277
|
+
'same_fam': same_fam
|
|
278
|
+
})
|
|
279
|
+
taxa_hsig_dict ={}
|
|
280
|
+
'''
|
|
281
|
+
### change info for allvsall
|
|
282
|
+
if allvsall:
|
|
283
|
+
for level, network_id_to_hid in taxa_networks_dict.items():
|
|
284
|
+
all_raw_profiles = p.pull_hashes(network_id_to_hid.values()) # returns hashid to hashsig dict
|
|
285
|
+
### turn into subhog to hashsig
|
|
286
|
+
network_id_to_hsig = {subhogid:all_raw_profiles[hid] for subhogid, hid in network_id_to_hid.items()}
|
|
287
|
+
taxa_hsig_dict[level] = [level_to_T_dict[level],network_id_to_hsig]
|
|
288
|
+
'''
|
|
289
|
+
hits_df = pd.DataFrame(rows)
|
|
290
|
+
|
|
291
|
+
if allvsall:
|
|
292
|
+
target_hogs_per_level = hits_df.groupby('taxid')['target_hog'].unique().to_dict()
|
|
293
|
+
for level, target_hogs in target_hogs_per_level.items():
|
|
294
|
+
query_hogs = hits_df[hits_df['taxid'] == level]['query_hog'].unique()
|
|
295
|
+
target_hogs = np.concatenate([target_hogs, query_hogs])
|
|
296
|
+
level_hashids = get_hashids_by_subhogid(list(target_hogs), fam2orthoxml_file)
|
|
297
|
+
taxa_hsig_dict[level] = [level_to_T_dict[level], p.pull_hashes(level_hashids)]
|
|
298
|
+
|
|
299
|
+
if len(no_hits_subhogs)>0:
|
|
300
|
+
print(f"No hits found for {len(no_hits_subhogs)} subHOGs")
|
|
301
|
+
print("Examples:", no_hits_subhogs[:5])
|
|
302
|
+
else:
|
|
303
|
+
print("All subHOGs had hits")
|
|
304
|
+
print("Total hits found:", len(hits_df))
|
|
305
|
+
print(hits_df.head())
|
|
306
|
+
|
|
307
|
+
### add column with coevolution origin
|
|
308
|
+
hits_df = find_coevolution_origin(hits_df, treepath)
|
|
309
|
+
# remove taxid column if it exists
|
|
310
|
+
if 'taxid' in hits_df.columns:
|
|
311
|
+
hits_df.drop(columns=['taxid'], inplace=True)
|
|
312
|
+
# replace nan taxname with treeroot if specified
|
|
313
|
+
if treeroot:
|
|
314
|
+
hits_df['taxname'] = hits_df['taxname'].replace(r'^\s*$', np.nan, regex=True)
|
|
315
|
+
hits_df['taxname'] = hits_df['taxname'].fillna(treeroot)
|
|
316
|
+
taxid_to_taxname_dict[0] = treeroot
|
|
317
|
+
print(hits_df.head())
|
|
318
|
+
### save to file
|
|
319
|
+
hits_df.to_csv(outputfile, index=False)
|
|
320
|
+
print("Created file:", outputfile)
|
|
321
|
+
return taxa_hsig_dict, taxid_to_taxname_dict
|
|
322
|
+
|
|
323
|
+
### from Dave's jupyter notebook, with changes
|
|
324
|
+
def compare_all_vs_all(taxa_hsig_dict, outputfile):
|
|
325
|
+
print("Performing all vs all comparison...")
|
|
326
|
+
for level, [level_T, network_id_to_hsig] in taxa_hsig_dict.items():
|
|
327
|
+
out_eps = outputfile.replace(".csv",f"{level}.eps")
|
|
328
|
+
ids = list(network_id_to_hsig.keys())
|
|
329
|
+
sigs = [network_id_to_hsig[i] for i in ids]
|
|
330
|
+
jkern = np.array([
|
|
331
|
+
[sig1.jaccard(sig2) if i != j else 0
|
|
332
|
+
for j, sig2 in enumerate(sigs)]
|
|
333
|
+
for i, sig1 in enumerate(sigs)
|
|
334
|
+
])
|
|
335
|
+
# convert to distance for clustering
|
|
336
|
+
dist = 1 - jkern
|
|
337
|
+
### now apply threshold
|
|
338
|
+
jkern_plot = jkern.copy()
|
|
339
|
+
jkern_plot[jkern_plot < level_T] = 0
|
|
340
|
+
print(jkern)
|
|
341
|
+
g = sns.clustermap(
|
|
342
|
+
dist,
|
|
343
|
+
xticklabels=ids,
|
|
344
|
+
yticklabels=ids,
|
|
345
|
+
figsize=(20, 20),
|
|
346
|
+
cmap="viridis_r" # reversed so small distance = high similarity
|
|
347
|
+
)
|
|
348
|
+
g.savefig(out_eps, format='eps')
|
|
349
|
+
print(f"Created file: {out_eps}")
|
|
350
|
+
|
|
351
|
+
'''original idea to get the matrix'''
|
|
352
|
+
def jaccard_similarity_matrix(sigs):
|
|
353
|
+
return np.array([[sig1.jaccard(sig2) for sig2 in sigs]
|
|
354
|
+
for sig1 in sigs])
|
|
355
|
+
|
|
356
|
+
'''faster way to get the matrix'''
|
|
357
|
+
def jaccard_similarity_matrix(sigs):
|
|
358
|
+
### Get the hash values of all signatures as a 2D array
|
|
359
|
+
hashvalues = np.array([sig.hashvalues for sig in sigs])
|
|
360
|
+
#print("hashvalues shape:", hashvalues.shape)
|
|
361
|
+
### Compare all pairs of signatures
|
|
362
|
+
# Vectorized pairwise Jaccard from matching MinHash values
|
|
363
|
+
jkern = np.mean(hashvalues[:, np.newaxis, :, :] ==
|
|
364
|
+
hashvalues[np.newaxis, :, :, :],
|
|
365
|
+
axis=(2, 3))
|
|
366
|
+
#print("jkern shape:", jkern.shape)
|
|
367
|
+
return jkern
|
|
368
|
+
|
|
369
|
+
'''visualize the similarity matrix as a heatmap with hierarchical clustering'''
|
|
370
|
+
def make_similarity_heatmap(jkern, ids, taxname, outputfile,format="png"):
|
|
371
|
+
dist = 1 - jkern
|
|
372
|
+
# Convert square distance matrix to condensed form for scipy
|
|
373
|
+
condensed_dist = squareform(dist, checks=False)
|
|
374
|
+
# Hierarchical clustering from the precomputed distances
|
|
375
|
+
linkage_matrix = linkage(condensed_dist, method="average")
|
|
376
|
+
g = sns.clustermap(
|
|
377
|
+
jkern,
|
|
378
|
+
row_linkage=linkage_matrix,
|
|
379
|
+
col_linkage=linkage_matrix,
|
|
380
|
+
xticklabels=ids,
|
|
381
|
+
yticklabels=ids,
|
|
382
|
+
figsize=(20, 20),
|
|
383
|
+
cmap="viridis",
|
|
384
|
+
vmin=0,
|
|
385
|
+
vmax=1
|
|
386
|
+
)
|
|
387
|
+
### only run eps for quite small datasets
|
|
388
|
+
if format == "eps":
|
|
389
|
+
outputfile = outputfile.replace(".csv", f"_{taxname}.eps")
|
|
390
|
+
g.savefig(outputfile, format="eps", bbox_inches='tight')
|
|
391
|
+
### png is a good default for larger datasets
|
|
392
|
+
elif format == "png":
|
|
393
|
+
outputfile = outputfile.replace(".csv", f"_{taxname}.png")
|
|
394
|
+
g.savefig(outputfile, dpi=600, format="png", bbox_inches='tight')
|
|
395
|
+
else:
|
|
396
|
+
raise ValueError(f"Unsupported format: {format}")
|
|
397
|
+
print(f"Created file: {outputfile}")
|
|
398
|
+
|
|
399
|
+
'''compare all vs all for each taxonomic level'''
|
|
400
|
+
def compare_all_vs_all(taxa_hsig_dict, taxid_to_taxname_dict,outputfile, heatmap=False):
|
|
401
|
+
print("\nPerforming all vs all comparison (untested)...")
|
|
402
|
+
#print(taxa_hsig_dict)
|
|
403
|
+
print(f"Using {len(taxa_hsig_dict)} taxonomic levels for comparison")
|
|
404
|
+
n_profiles = sum(len(network_id_to_hsig)
|
|
405
|
+
for level_T, network_id_to_hsig in taxa_hsig_dict.values())
|
|
406
|
+
print(f"Using {n_profiles} profiles for comparison")
|
|
407
|
+
for level, [level_T, network_id_to_hsig] in tqdm(taxa_hsig_dict.items(),
|
|
408
|
+
desc="Taxonomic levels"):
|
|
409
|
+
taxname = taxid_to_taxname_dict.get(level, f"taxid_{level}")
|
|
410
|
+
ids = list(network_id_to_hsig.keys())
|
|
411
|
+
if len(ids) < 2:
|
|
412
|
+
print(f"Skipping {level}: only {len(ids)} profile(s)")
|
|
413
|
+
continue
|
|
414
|
+
sigs = [network_id_to_hsig[i] for i in ids]
|
|
415
|
+
# similarity matrix
|
|
416
|
+
jkern = jaccard_similarity_matrix(sigs)
|
|
417
|
+
np.fill_diagonal(jkern, 1.0)
|
|
418
|
+
# distance for clustering
|
|
419
|
+
#dist = 1 - jkern
|
|
420
|
+
# --- threshold ONLY for display ---
|
|
421
|
+
#jkern_plot = jkern.copy()
|
|
422
|
+
#jkern_plot[jkern_plot < level_T] = 0
|
|
423
|
+
# threshold only for exported similarity matrix
|
|
424
|
+
sim_df = pd.DataFrame(jkern, index=ids, columns=ids)
|
|
425
|
+
sim_df[sim_df < level_T] = 0
|
|
426
|
+
### save to csv
|
|
427
|
+
sim_csv = outputfile.replace(".csv", f"_{level}_similarity_matrix.csv")
|
|
428
|
+
sim_df.to_csv(sim_csv)
|
|
429
|
+
print(f"Created similarity matrix file: {sim_csv}")
|
|
430
|
+
if heatmap:
|
|
431
|
+
make_similarity_heatmap(jkern, ids, taxname, outputfile)
|
|
432
|
+
|
|
433
|
+
def main(lshforestpath, hashes_h5, treepath, outputfile, profiler_path, queries_file,thresholds_file,
|
|
434
|
+
allvsall = False, heatmap=False, k= 1000, treeroot=""):
|
|
435
|
+
### print arguments
|
|
436
|
+
print("\n📋 Running with the following parameters:")
|
|
437
|
+
print(f" • LSH Forest path: {lshforestpath}")
|
|
438
|
+
print(f" • Hashes HDF5: {hashes_h5}")
|
|
439
|
+
print(f" • Tree path: {treepath}")
|
|
440
|
+
print(f" • Output file: {outputfile}")
|
|
441
|
+
print(f" • Profiler path: {profiler_path}")
|
|
442
|
+
print(f" • Queries file: {queries_file}")
|
|
443
|
+
print(f" • All vs all: {allvsall}")
|
|
444
|
+
print(f" • Heatmap: {heatmap}")
|
|
445
|
+
print(f" • k: {k}")
|
|
446
|
+
print(f" • thresholds table: {thresholds_file}")
|
|
447
|
+
print("\n")
|
|
448
|
+
|
|
449
|
+
### if thresholds are given, take them into account
|
|
450
|
+
thresholds_df = pd.read_csv(thresholds_file)[['bin','empirical_t','taxname']]
|
|
451
|
+
thresholds_df.set_index('bin', inplace=True)
|
|
452
|
+
|
|
453
|
+
create_directory(os.path.dirname(outputfile))
|
|
454
|
+
|
|
455
|
+
### get hits from hashes
|
|
456
|
+
taxa_hsig_dict, taxid_to_taxname_dict = extract_hits_from_hashes(lshforestpath, hashes_h5, treepath, outputfile, profiler_path,
|
|
457
|
+
allvsall, k, queries_file, thresholds_df, treeroot=treeroot)
|
|
458
|
+
if heatmap and not allvsall:
|
|
459
|
+
print("Warning: heatmap option is only relevant for allvsall mode. Ignoring heatmap option.")
|
|
460
|
+
if allvsall:
|
|
461
|
+
compare_all_vs_all(taxa_hsig_dict, taxid_to_taxname_dict, heatmap=heatmap, outputfile=outputfile.replace(".csv", "_allvsall.csv"))
|
|
462
|
+
|
|
463
|
+
## Command line argument parsing'''
|
|
464
|
+
def parse_args():
|
|
465
|
+
"""
|
|
466
|
+
Parse command line arguments.
|
|
467
|
+
"""
|
|
468
|
+
print("Parsing command line arguments")
|
|
469
|
+
parser = argparse.ArgumentParser(description="Extract hits from the profiler for levels mode, per taxon. Queries are subHOGs that can be found in the fam2orthoxml file.")
|
|
470
|
+
parser.add_argument("-i", "--input", required=True, help="HogProf output folder")
|
|
471
|
+
parser.add_argument("-o", "--outputfile", required=True, help="Output CSV file")
|
|
472
|
+
parser.add_argument("--queries_file", required=True, help="Query subHOG table")
|
|
473
|
+
parser.add_argument("--thresholds", required=True, help="Threshold CSV from Step 3")
|
|
474
|
+
parser.add_argument("--profilerpath", default="profiler.py")
|
|
475
|
+
parser.add_argument("--k", type=int, default=1000, help="Number of top hits to extract for each query.")
|
|
476
|
+
parser.add_argument("--allvsall",type=bool, help="Run all vs all mode additionally to the main hit extraction (may take a long time).", default=False)
|
|
477
|
+
parser.add_argument("--heatmap",type=bool, help="Generate a heatmap of the all vs all comparisons.", default=False)
|
|
478
|
+
parser.add_argument("--treeroot", default="", help="Root of the tree to be used in output table as a taxon name. " \
|
|
479
|
+
"The root is often called 'internal_0' so by default, if there is no taxon name it would get an empty string in " \
|
|
480
|
+
"the results table. Specify it here to avoid this")
|
|
481
|
+
return parser.parse_args()
|
|
482
|
+
|
|
483
|
+
if __name__ == "__main__":
|
|
484
|
+
# Parse command line arguments
|
|
485
|
+
args = parse_args()
|
|
486
|
+
#print("Are arguments fixed?")
|
|
487
|
+
inputfolder = args.input
|
|
488
|
+
lshforestpath = os.path.join(inputfolder, "newlshforest.pkl")
|
|
489
|
+
hashes_h5 = os.path.join(inputfolder, "hashes.h5")
|
|
490
|
+
treepath = os.path.join(inputfolder, "speciestree.nwk")
|
|
491
|
+
treepath = os.path.join(inputfolder, "reformatted_tree.nwk")
|
|
492
|
+
fam2orthoxml_file = os.path.join(inputfolder, "fam2orthoxml.csv")
|
|
493
|
+
queries_file = args.queries_file
|
|
494
|
+
# special case for unknown hashids (subhogid per line, identical to fam2orthoxml)
|
|
495
|
+
queries_file = get_hashids(queries_file, fam2orthoxml_file)
|
|
496
|
+
# Run the main function with the parsed arguments
|
|
497
|
+
main(
|
|
498
|
+
lshforestpath=lshforestpath,
|
|
499
|
+
hashes_h5=hashes_h5,
|
|
500
|
+
treepath=treepath,
|
|
501
|
+
outputfile=args.outputfile,
|
|
502
|
+
profiler_path = args.profilerpath,
|
|
503
|
+
allvsall=args.allvsall,
|
|
504
|
+
heatmap=args.heatmap,
|
|
505
|
+
k=args.k,
|
|
506
|
+
queries_file = queries_file,
|
|
507
|
+
thresholds_file=args.thresholds,
|
|
508
|
+
treeroot=args.treeroot
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
### print end time
|
|
512
|
+
end_time_secs = time()
|
|
513
|
+
end_time = datetime.fromtimestamp(end_time_secs).strftime('%Y-%m-%d %H:%M:%S')
|
|
514
|
+
print(f"Script finished at: {end_time}\n")
|
|
515
|
+
execution_time = end_time_secs - start_time_secs
|
|
516
|
+
### print execution time but reformat it to be human readable
|
|
517
|
+
print(f"Total execution time: {round(execution_time, 2)} seconds\n")
|
|
518
|
+
print("\nDONE!\n")
|