Marco Martínez González commited on
Commit
1ed75d7
·
1 Parent(s): 2b02bf3

Add isim method to SimilarityIndex

Browse files
Files changed (1) hide show
  1. 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
- order: int = 1,
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. The ideas
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. The ideas
94
- supporting this method are described in the following papers:
95
- https://jcheminf.biomedcentral.com/articles/10.1186/s13321-021-00505-3
96
- https://jcheminf.biomedcentral.com/articles/10.1186/s13321-021-00504-4
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
- # check if the w_factor and c_threshold are valid
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.order = order
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 is valid
485
- if c_threshold not in ["dissimilar", None]:
486
- if not isinstance(c_threshold, int):
487
- raise ValueError(
488
- f'Invalid c_threshold. It must be an integer or "dissimilar" or None. '
489
- f"Given c_threshold = {c_threshold}"
490
- )
491
- # check if the w_factor is valid
492
- if w_factor != "fraction":
493
- if w_factor.split("_")[0] != "power" or not w_factor.split("_")[-1].isdigit():
494
- print(
495
- f'Invalid weight factor "{w_factor}" given. Using default value '
496
- '"similarity = dissimilarity = 1".'
497
- )
498
- w_factor = False
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
- n_objects = len(arr)
541
-
542
- # Assign c_threshold
543
- if self.c_threshold is None:
544
- tmp_c_threshold = n_objects % 2
545
- elif self.c_threshold == "dissimilar":
546
- tmp_c_threshold = math.ceil(n_objects / 2)
547
- elif isinstance(self.c_threshold, int):
548
- if self.c_threshold >= n_objects:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  raise ValueError(
550
- "c_threshold cannot be equal or greater than n_objects. \n"
551
- f"c_threshold = {self.c_threshold} n_objects = {n_objects}"
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
- # Calculate a, d, b + c
561
- # Calculate the positions (columns) of common on bits (common 1s) between the objects
562
- a_indices = 2 * c_total - n_objects > tmp_c_threshold
563
- # Calculate the positions (columns) common off bits (common 0s) between the objects
564
- d_indices = n_objects - 2 * c_total > tmp_c_threshold
565
- # Calculate the positions (columns) of dissimilar bits between the objects (b + c)
566
- # the dissimilar bits are the bits that are not common between the objects
567
- dis_indices = np.abs(2 * c_total - n_objects) <= tmp_c_threshold
568
-
569
- # Calculate the number of columns with common on bits (common 1s) between the objects
570
- a = np.sum(a_indices)
571
- # Calculate the number of columns with common off bits (common 0s) between the objects
572
- d = np.sum(d_indices)
573
- # Calculate the number of columns with dissimilar bits between the objects (b + c)
574
- total_dis = np.sum(dis_indices)
575
-
576
- # calculate the weights for each column indexed as with common on bits (common 1s)
577
- a_w_array = self._f_s(2 * c_total[a_indices] - n_objects, n_objects)
578
- # calculate the weights for each column indexed as with common off bits (common 0s)
579
- d_w_array = self._f_s(abs(2 * c_total[d_indices] - n_objects), n_objects)
580
- # calculate the weights for each column indexed as with dissimilar bits
581
- total_w_dis_array = self._f_d(abs(2 * c_total[dis_indices] - n_objects), n_objects)
582
-
583
- # calculate the total weight for each type of counter
584
- w_a = np.sum(a_w_array)
585
- w_d = np.sum(d_w_array)
586
- total_w_dis = np.sum(total_w_dis_array)
587
-
588
- # calculate the counters needed to calculate the similarity indexes
589
- total_sim = a + d
590
- total_w_sim = w_a + w_d
591
- p = total_sim + total_dis
592
- w_p = total_w_sim + total_w_dis
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,