Spaces:
Sleeping
Sleeping
Marco Martínez González commited on
Commit ·
1ed75d7
1
Parent(s): 2b02bf3
Add isim method to SimilarityIndex
Browse files- selector/methods/similarity.py +146 -84
selector/methods/similarity.py
CHANGED
|
@@ -29,8 +29,11 @@ time and return a value between 0 and 1, where 0 means that all the molecules in
|
|
| 29 |
completely different and 1 means that the molecules are identical.
|
| 30 |
|
| 31 |
The ideas behind the similarity-based selection methods are described in the following papers:
|
|
|
|
| 32 |
https://jcheminf.biomedcentral.com/articles/10.1186/s13321-021-00505-3
|
| 33 |
https://jcheminf.biomedcentral.com/articles/10.1186/s13321-021-00504-4
|
|
|
|
|
|
|
| 34 |
|
| 35 |
"""
|
| 36 |
|
|
@@ -38,6 +41,7 @@ import math
|
|
| 38 |
import random
|
| 39 |
from math import log
|
| 40 |
from typing import List, Optional, Union
|
|
|
|
| 41 |
|
| 42 |
import numpy as np
|
| 43 |
|
|
@@ -62,13 +66,13 @@ class NSimilarity(SelectionBase):
|
|
| 62 |
Notes
|
| 63 |
-----
|
| 64 |
The ideas behind the similarity-based selection methods are described in the following papers:
|
| 65 |
-
|
| 66 |
"""
|
| 67 |
|
| 68 |
def __init__(
|
| 69 |
self,
|
| 70 |
method: str = "isim",
|
| 71 |
-
|
| 72 |
similarity_index: str = "RR",
|
| 73 |
w_factor: str = "fraction",
|
| 74 |
c_threshold: Union[None, str, int] = None,
|
|
@@ -84,20 +88,14 @@ class NSimilarity(SelectionBase):
|
|
| 84 |
The instant similarity index calculates the average similarity of a set of
|
| 85 |
objects without the need to calculate the similarity of all the possible pairs.
|
| 86 |
This method is easier to use than the ``esim`` (extended similarity index) as it
|
| 87 |
-
does not require the use of weight factors or coincidence thresholds.
|
| 88 |
-
supporting this method are described in the following papers:
|
| 89 |
-
- TODO: Add paper
|
| 90 |
- "esim": Extended Similarity
|
| 91 |
The extended similarity index calculates the similarity of a set of objects
|
| 92 |
without the need to calculate the similarity of all the possible pairs. This
|
| 93 |
-
method requires the use of weight factors and coincidence thresholds.
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
https://link.springer.com/article/10.1007/s10822-022-00444-7
|
| 98 |
-
order: int
|
| 99 |
-
Integer indicating the 1/order power used to approximate the average of the
|
| 100 |
-
similarity values elevated to 1/order.
|
| 101 |
similarity_index: str
|
| 102 |
Key with the abbreviation of the similarity index that will be used to perform the
|
| 103 |
selection.
|
|
@@ -137,9 +135,7 @@ class NSimilarity(SelectionBase):
|
|
| 137 |
"""
|
| 138 |
# check if the method is valid
|
| 139 |
if method not in ["isim", "esim"]:
|
| 140 |
-
raise ValueError(
|
| 141 |
-
f'Method "{method}" is not available please select "isim" or "esim".'
|
| 142 |
-
)
|
| 143 |
# check if the similarity index is valid
|
| 144 |
if similarity_index not in _similarity_index_dict:
|
| 145 |
raise ValueError(
|
|
@@ -148,7 +144,7 @@ class NSimilarity(SelectionBase):
|
|
| 148 |
)
|
| 149 |
# for the esim method, check if the w_factor is valid
|
| 150 |
if method == "esim":
|
| 151 |
-
|
| 152 |
if w_factor != "fraction":
|
| 153 |
if w_factor.split("_")[0] != "power" or not w_factor.split("_")[-1].isdigit():
|
| 154 |
print(
|
|
@@ -165,7 +161,7 @@ class NSimilarity(SelectionBase):
|
|
| 165 |
)
|
| 166 |
|
| 167 |
self.method = method
|
| 168 |
-
self.
|
| 169 |
self.similarity_index = similarity_index
|
| 170 |
self.w_factor = w_factor
|
| 171 |
self.c_threshold = c_threshold
|
|
@@ -257,6 +253,8 @@ class NSimilarity(SelectionBase):
|
|
| 257 |
# create an instance of the SimilarityIndex class. It is used to calculate the similarity
|
| 258 |
# index of the sets of selected objects.
|
| 259 |
similarity_index = SimilarityIndex(
|
|
|
|
|
|
|
| 260 |
similarity_index=self.similarity_index,
|
| 261 |
c_threshold=self.c_threshold,
|
| 262 |
w_factor=self.w_factor,
|
|
@@ -343,6 +341,8 @@ class NSimilarity(SelectionBase):
|
|
| 343 |
# create an instance of the SimilarityIndex class. It is used to calculate the medoid and
|
| 344 |
# the outlier of the data.
|
| 345 |
similarity_index = SimilarityIndex(
|
|
|
|
|
|
|
| 346 |
similarity_index=self.similarity_index,
|
| 347 |
c_threshold=self.c_threshold,
|
| 348 |
w_factor=self.w_factor,
|
|
@@ -430,6 +430,8 @@ class SimilarityIndex:
|
|
| 430 |
|
| 431 |
def __init__(
|
| 432 |
self,
|
|
|
|
|
|
|
| 433 |
similarity_index: str = "RR",
|
| 434 |
c_threshold: Union[None, str, int] = None,
|
| 435 |
w_factor: str = "fraction",
|
|
@@ -438,6 +440,23 @@ class SimilarityIndex:
|
|
| 438 |
|
| 439 |
Parameters
|
| 440 |
----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
similarity_index : str, optional
|
| 442 |
The key with the abbreviation of the similarity index to be used for calculations.
|
| 443 |
Possible values are:
|
|
@@ -475,28 +494,34 @@ class SimilarityIndex:
|
|
| 475 |
Default is 'fraction'.
|
| 476 |
other values : similarity = dissimilarity = 1
|
| 477 |
"""
|
|
|
|
|
|
|
|
|
|
| 478 |
# check if the similarity index is valid
|
| 479 |
if similarity_index not in _similarity_index_dict:
|
| 480 |
raise ValueError(
|
| 481 |
f'Similarity index "{similarity_index}" is not available. '
|
| 482 |
f"See the documentation for the available similarity indexes."
|
| 483 |
)
|
| 484 |
-
# check if the c_threshold
|
| 485 |
-
if
|
| 486 |
-
if
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
|
|
|
|
|
|
|
|
|
| 500 |
self.similarity_index = similarity_index
|
| 501 |
self.w_factor = w_factor
|
| 502 |
self.c_threshold = c_threshold
|
|
@@ -537,59 +562,96 @@ class SimilarityIndex:
|
|
| 537 |
)
|
| 538 |
else:
|
| 539 |
c_total = np.sum(arr, axis=0)
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
raise ValueError(
|
| 550 |
-
"c_threshold
|
| 551 |
-
f"c_threshold = {self.c_threshold}
|
| 552 |
)
|
| 553 |
-
tmp_c_threshold = self.c_threshold
|
| 554 |
-
else:
|
| 555 |
-
raise ValueError(
|
| 556 |
-
"c_threshold must be None, 'dissimilar' or an integer. \n"
|
| 557 |
-
f"Given c_threshold = {self.c_threshold}"
|
| 558 |
-
)
|
| 559 |
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
|
| 594 |
counters = {
|
| 595 |
"a": a,
|
|
|
|
| 29 |
completely different and 1 means that the molecules are identical.
|
| 30 |
|
| 31 |
The ideas behind the similarity-based selection methods are described in the following papers:
|
| 32 |
+
(esim)
|
| 33 |
https://jcheminf.biomedcentral.com/articles/10.1186/s13321-021-00505-3
|
| 34 |
https://jcheminf.biomedcentral.com/articles/10.1186/s13321-021-00504-4
|
| 35 |
+
(isim)
|
| 36 |
+
TODO: Add paper
|
| 37 |
|
| 38 |
"""
|
| 39 |
|
|
|
|
| 41 |
import random
|
| 42 |
from math import log
|
| 43 |
from typing import List, Optional, Union
|
| 44 |
+
import warnings
|
| 45 |
|
| 46 |
import numpy as np
|
| 47 |
|
|
|
|
| 66 |
Notes
|
| 67 |
-----
|
| 68 |
The ideas behind the similarity-based selection methods are described in the following papers:
|
| 69 |
+
|
| 70 |
"""
|
| 71 |
|
| 72 |
def __init__(
|
| 73 |
self,
|
| 74 |
method: str = "isim",
|
| 75 |
+
inv_order: int = 1,
|
| 76 |
similarity_index: str = "RR",
|
| 77 |
w_factor: str = "fraction",
|
| 78 |
c_threshold: Union[None, str, int] = None,
|
|
|
|
| 88 |
The instant similarity index calculates the average similarity of a set of
|
| 89 |
objects without the need to calculate the similarity of all the possible pairs.
|
| 90 |
This method is easier to use than the ``esim`` (extended similarity index) as it
|
| 91 |
+
does not require the use of weight factors or coincidence thresholds.
|
|
|
|
|
|
|
| 92 |
- "esim": Extended Similarity
|
| 93 |
The extended similarity index calculates the similarity of a set of objects
|
| 94 |
without the need to calculate the similarity of all the possible pairs. This
|
| 95 |
+
method requires the use of weight factors and coincidence thresholds.
|
| 96 |
+
inv_order: int
|
| 97 |
+
Integer indicating the 1/inv_order power used to approximate the average of the
|
| 98 |
+
similarity values elevated to 1/inv_order.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
similarity_index: str
|
| 100 |
Key with the abbreviation of the similarity index that will be used to perform the
|
| 101 |
selection.
|
|
|
|
| 135 |
"""
|
| 136 |
# check if the method is valid
|
| 137 |
if method not in ["isim", "esim"]:
|
| 138 |
+
raise ValueError(f'Method "{method}" is not available please select "isim" or "esim".')
|
|
|
|
|
|
|
| 139 |
# check if the similarity index is valid
|
| 140 |
if similarity_index not in _similarity_index_dict:
|
| 141 |
raise ValueError(
|
|
|
|
| 144 |
)
|
| 145 |
# for the esim method, check if the w_factor is valid
|
| 146 |
if method == "esim":
|
| 147 |
+
# check if the w_factor and c_threshold are valid
|
| 148 |
if w_factor != "fraction":
|
| 149 |
if w_factor.split("_")[0] != "power" or not w_factor.split("_")[-1].isdigit():
|
| 150 |
print(
|
|
|
|
| 161 |
)
|
| 162 |
|
| 163 |
self.method = method
|
| 164 |
+
self.inv_order = inv_order
|
| 165 |
self.similarity_index = similarity_index
|
| 166 |
self.w_factor = w_factor
|
| 167 |
self.c_threshold = c_threshold
|
|
|
|
| 253 |
# create an instance of the SimilarityIndex class. It is used to calculate the similarity
|
| 254 |
# index of the sets of selected objects.
|
| 255 |
similarity_index = SimilarityIndex(
|
| 256 |
+
method=self.method,
|
| 257 |
+
inv_order=self.inv_order,
|
| 258 |
similarity_index=self.similarity_index,
|
| 259 |
c_threshold=self.c_threshold,
|
| 260 |
w_factor=self.w_factor,
|
|
|
|
| 341 |
# create an instance of the SimilarityIndex class. It is used to calculate the medoid and
|
| 342 |
# the outlier of the data.
|
| 343 |
similarity_index = SimilarityIndex(
|
| 344 |
+
method=self.method,
|
| 345 |
+
inv_order=self.inv_order,
|
| 346 |
similarity_index=self.similarity_index,
|
| 347 |
c_threshold=self.c_threshold,
|
| 348 |
w_factor=self.w_factor,
|
|
|
|
| 430 |
|
| 431 |
def __init__(
|
| 432 |
self,
|
| 433 |
+
method: str = "isim",
|
| 434 |
+
inv_order: int = 1,
|
| 435 |
similarity_index: str = "RR",
|
| 436 |
c_threshold: Union[None, str, int] = None,
|
| 437 |
w_factor: str = "fraction",
|
|
|
|
| 440 |
|
| 441 |
Parameters
|
| 442 |
----------
|
| 443 |
+
method : {"isim", "esim"}, optional
|
| 444 |
+
Method used for calculating the similarity indexes. The methods are:
|
| 445 |
+
- "isim": Instant Similarity
|
| 446 |
+
The instant similarity index calculates the average similarity of a set of
|
| 447 |
+
objects without the need to calculate the similarity of all the possible pairs.
|
| 448 |
+
This method is easier to use than the ``esim`` (extended similarity index) as it
|
| 449 |
+
does not require the use of weight factors or coincidence thresholds.
|
| 450 |
+
- "esim": Extended Similarity
|
| 451 |
+
The extended similarity index calculates the similarity of a set of objects
|
| 452 |
+
without the need to calculate the similarity of all the possible pairs. This
|
| 453 |
+
method requires the use of weight factors and coincidence thresholds.
|
| 454 |
+
Default is "isim".
|
| 455 |
+
|
| 456 |
+
k : int, optional
|
| 457 |
+
Integer indicating the 1/k power used to approximate the average of the
|
| 458 |
+
similarity values elevated to 1/k. Default is 1.
|
| 459 |
+
|
| 460 |
similarity_index : str, optional
|
| 461 |
The key with the abbreviation of the similarity index to be used for calculations.
|
| 462 |
Possible values are:
|
|
|
|
| 494 |
Default is 'fraction'.
|
| 495 |
other values : similarity = dissimilarity = 1
|
| 496 |
"""
|
| 497 |
+
# check if the method is valid
|
| 498 |
+
if method not in ["isim", "esim"]:
|
| 499 |
+
raise ValueError(f'Method "{method}" is not available please select "isim" or "esim".')
|
| 500 |
# check if the similarity index is valid
|
| 501 |
if similarity_index not in _similarity_index_dict:
|
| 502 |
raise ValueError(
|
| 503 |
f'Similarity index "{similarity_index}" is not available. '
|
| 504 |
f"See the documentation for the available similarity indexes."
|
| 505 |
)
|
| 506 |
+
# for the esim method, check if the w_factor and c_threshold are valid
|
| 507 |
+
if method == "esim":
|
| 508 |
+
# check if the c_threshold is valid
|
| 509 |
+
if c_threshold not in ["dissimilar", None]:
|
| 510 |
+
if not isinstance(c_threshold, int):
|
| 511 |
+
raise ValueError(
|
| 512 |
+
f'Invalid c_threshold. It must be an integer or "dissimilar" or None. '
|
| 513 |
+
f"Given c_threshold = {c_threshold}"
|
| 514 |
+
)
|
| 515 |
+
# check if the w_factor is valid
|
| 516 |
+
if w_factor != "fraction":
|
| 517 |
+
if w_factor.split("_")[0] != "power" or not w_factor.split("_")[-1].isdigit():
|
| 518 |
+
print(
|
| 519 |
+
f'Invalid weight factor "{w_factor}" given. Using default value '
|
| 520 |
+
'"similarity = dissimilarity = 1".'
|
| 521 |
+
)
|
| 522 |
+
w_factor = False
|
| 523 |
+
self.method = method
|
| 524 |
+
self.inv_order = inv_order
|
| 525 |
self.similarity_index = similarity_index
|
| 526 |
self.w_factor = w_factor
|
| 527 |
self.c_threshold = c_threshold
|
|
|
|
| 562 |
)
|
| 563 |
else:
|
| 564 |
c_total = np.sum(arr, axis=0)
|
| 565 |
+
len_arr = len(arr)
|
| 566 |
+
if n_objects and n_objects != len_arr:
|
| 567 |
+
warnings.warn(
|
| 568 |
+
f"Warning, specified number of objects {n_objects} is different from the number"
|
| 569 |
+
" of objects in data {len_arr}\n"
|
| 570 |
+
"Doing calculations with",
|
| 571 |
+
n_objects,
|
| 572 |
+
"objects.",
|
| 573 |
+
)
|
| 574 |
+
n_objects = len_arr
|
| 575 |
+
|
| 576 |
+
# if method is isim, calculate the counters using the instant similarity index
|
| 577 |
+
if self.method == "isim":
|
| 578 |
+
# calculate number of instances with common on bits (common 1s) for each column
|
| 579 |
+
a_array = c_total * (c_total - 1) / 2
|
| 580 |
+
# calculate number of instances with common off bits (common 0s) for each column
|
| 581 |
+
off_coincidences = n_objects - c_total
|
| 582 |
+
d_array = off_coincidences * (off_coincidences - 1) / 2
|
| 583 |
+
# calculate number of instances with dissimilar bits for each column
|
| 584 |
+
dis_array = off_coincidences * c_total
|
| 585 |
+
|
| 586 |
+
# calculate total a, d, b + c counters
|
| 587 |
+
a = np.sum(np.power(a_array, 1.0 / self.inv_order))
|
| 588 |
+
d = np.sum(np.power(d_array, 1.0 / self.inv_order))
|
| 589 |
+
total_dis = np.sum(np.power(dis_array, 1 / self.inv_order))
|
| 590 |
+
|
| 591 |
+
# calculate total similarity, and total similarity + dissimilarity
|
| 592 |
+
total_sim = a + d
|
| 593 |
+
p = total_sim + total_dis
|
| 594 |
+
|
| 595 |
+
# in the isim method, the counters are not weighted, so the weighted counters are equal
|
| 596 |
+
# to the non-weighted counters
|
| 597 |
+
w_a = a
|
| 598 |
+
w_d = d
|
| 599 |
+
total_w_sim = total_sim
|
| 600 |
+
total_w_dis = total_dis
|
| 601 |
+
w_p = p
|
| 602 |
+
|
| 603 |
+
elif self.method == "esim":
|
| 604 |
+
# Assign c_threshold
|
| 605 |
+
if self.c_threshold is None:
|
| 606 |
+
tmp_c_threshold = n_objects % 2
|
| 607 |
+
elif self.c_threshold == "dissimilar":
|
| 608 |
+
tmp_c_threshold = math.ceil(n_objects / 2)
|
| 609 |
+
elif isinstance(self.c_threshold, int):
|
| 610 |
+
if self.c_threshold >= n_objects:
|
| 611 |
+
raise ValueError(
|
| 612 |
+
"c_threshold cannot be equal or greater than n_objects. \n"
|
| 613 |
+
f"c_threshold = {self.c_threshold} n_objects = {n_objects}"
|
| 614 |
+
)
|
| 615 |
+
tmp_c_threshold = self.c_threshold
|
| 616 |
+
else:
|
| 617 |
raise ValueError(
|
| 618 |
+
"c_threshold must be None, 'dissimilar' or an integer. \n"
|
| 619 |
+
f"Given c_threshold = {self.c_threshold}"
|
| 620 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
|
| 622 |
+
# Calculate a, d, b + c
|
| 623 |
+
# Calculate the positions (columns) of common on bits (common 1s) between the objects
|
| 624 |
+
a_indices = 2 * c_total - n_objects > tmp_c_threshold
|
| 625 |
+
# Calculate the positions (columns) common off bits (common 0s) between the objects
|
| 626 |
+
d_indices = n_objects - 2 * c_total > tmp_c_threshold
|
| 627 |
+
# Calculate the positions (columns) of dissimilar bits between the objects (b + c)
|
| 628 |
+
# the dissimilar bits are the bits that are not common between the objects
|
| 629 |
+
dis_indices = np.abs(2 * c_total - n_objects) <= tmp_c_threshold
|
| 630 |
+
|
| 631 |
+
# Calculate the number of columns with common on bits (common 1s) between the objects
|
| 632 |
+
a = np.sum(np.power(a_indices, 1.0 / self.inv_order))
|
| 633 |
+
# Calculate the number of columns with common off bits (common 0s) between the objects
|
| 634 |
+
d = np.sum(np.power(d_indices, 1.0 / self.inv_order))
|
| 635 |
+
# Calculate the number of columns with dissimilar bits between the objects (b + c)
|
| 636 |
+
total_dis = np.sum(np.power(dis_indices, 1.0 / self.inv_order))
|
| 637 |
+
|
| 638 |
+
# calculate the weights for each column indexed as with common on bits (common 1s)
|
| 639 |
+
a_w_array = self._f_s(2 * c_total[a_indices] - n_objects, n_objects)
|
| 640 |
+
# calculate the weights for each column indexed as with common off bits (common 0s)
|
| 641 |
+
d_w_array = self._f_s(abs(2 * c_total[d_indices] - n_objects), n_objects)
|
| 642 |
+
# calculate the weights for each column indexed as with dissimilar bits
|
| 643 |
+
total_w_dis_array = self._f_d(abs(2 * c_total[dis_indices] - n_objects), n_objects)
|
| 644 |
+
|
| 645 |
+
# calculate the total weight for each type of counter
|
| 646 |
+
w_a = np.sum(np.power(a_w_array, 1.0 / self.inv_order))
|
| 647 |
+
w_d = np.sum(np.power(d_w_array, 1.0 / self.inv_order))
|
| 648 |
+
total_w_dis = np.sum(np.power(total_w_dis_array, 1.0 / self.inv_order))
|
| 649 |
+
|
| 650 |
+
# calculate the counters needed to calculate the similarity indexes
|
| 651 |
+
total_sim = a + d
|
| 652 |
+
total_w_sim = w_a + w_d
|
| 653 |
+
p = total_sim + total_dis
|
| 654 |
+
w_p = total_w_sim + total_w_dis
|
| 655 |
|
| 656 |
counters = {
|
| 657 |
"a": a,
|