MASSA-Algorithm 1.0.0__py3-none-any.whl → 2.0.1__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.
MASSA_Algorithm/MASSA.py CHANGED
@@ -1,3 +1,4 @@
1
+ import time
1
2
  from MASSA_Algorithm import MASSAlogos
2
3
  from MASSA_Algorithm import MASSAargs
3
4
  from MASSA_Algorithm import MASSAod
@@ -8,6 +9,8 @@ from MASSA_Algorithm import MASSApreparation
8
9
  from MASSA_Algorithm import MASSAcluster
9
10
  from MASSA_Algorithm import MASSAsplit
10
11
  from MASSA_Algorithm import MASSAmoloutput
12
+ from joblib import parallel_backend
13
+
11
14
 
12
15
 
13
16
  def returns_zero(total, test, validation):
@@ -48,10 +51,11 @@ def main():
48
51
  Main subroutine, allows the program to run directly from
49
52
  the command line when installed via pip.
50
53
  """
54
+ parallel_backend('multiprocessing')
51
55
  # Initializing from the command line.
52
56
  # Print the program logo:
53
57
  MASSAlogos.initial_print()
54
-
58
+ start_time = time.time()
55
59
  # It captures command line arguments.
56
60
  (file_input,
57
61
  file_output,
@@ -67,18 +71,32 @@ def main():
67
71
  linkage_method,
68
72
  flag_dendrogram,
69
73
  splitting_strategy,
70
- drop_errors) = MASSAargs.capture_args()
74
+ drop_errors,
75
+ large_library) = MASSAargs.capture_args()
71
76
  print('Initializing, wait...\n')
72
77
 
73
78
  # Create log.txt file in directory:
74
- arqlog = directoryfile_output+'/log.txt'
75
- writelog = open(arqlog, 'w')
79
+ log_path = directoryfile_output+'/log.txt'
80
+ write_log = open(log_path, 'w')
76
81
 
77
82
  # Initial file management:
78
- # It creates the output directories.
79
- MASSAod.output_directory(directoryfile_output)
80
83
  # Read molecules.
81
- mols = MASSAopen_files.read_molecules(file_input, writelog, drop_errors)
84
+ mols = MASSAopen_files.read_molecules(file_input, write_log, drop_errors)
85
+
86
+ # If auto detection is enable to select the initial cluster algorithm.
87
+ if large_library == 'auto':
88
+ if int(len(mols)) > 9999:
89
+ large_library = True
90
+ else:
91
+ large_library = False
92
+
93
+ if large_library == True:
94
+ # Deactivates the HCA dendrogram.
95
+ flag_dendrogram = False
96
+
97
+ # It creates the output directories.
98
+ MASSAod.output_directory(directoryfile_output, flag_dendrogram)
99
+
82
100
  # Extracting the property names from the ".sdf" input file.
83
101
  sdf_property_names = MASSAopen_files.get_sdf_property_names(mols)
84
102
  # Structure 3D management - It adds hydrogens keeping 3D coordenates.
@@ -87,6 +105,7 @@ def main():
87
105
  # Extraction properties from ".sdf":
88
106
  # It extracts the names of the molecules and creates a name:molecule dictionary and a dataframe.
89
107
  names, dataframe = MASSAextraction.name_extraction(mols_h)
108
+
90
109
  # It defines a list of what biological activities are being extracted.
91
110
  biological_activity = MASSAextraction.the_biological_handler(
92
111
  sdf_property_names, number_bioact, bioactivities_as_args)
@@ -112,16 +131,28 @@ def main():
112
131
  # PCA for the structural domain.
113
132
  fp_pca = MASSApreparation.pca_maker(fp_matrix, npcs, svd_parameter)
114
133
 
115
- # First clustering (HCA):
116
- # It performs HCA clustering without generating the dendrogram for the biological domain.
117
- leaves_cluster_bio, bio_hca, linkage_bio, cutoff_bio = MASSAcluster.hca_clusters(
118
- bio_pca, names, 'bio', directoryfile_output, extension_type, linkage_method)
119
- # It performs HCA clustering without generating the dendrogram for the physicochemical domain.
120
- leaves_cluster_phch, phch_hca, linkage_phch, cutoff_phch = MASSAcluster.hca_clusters(
121
- phch_pca, names, 'PhCh', directoryfile_output, extension_type, linkage_method)
122
- # It performs HCA clustering without generating the dendrogram for the structural domain.
123
- leaves_cluster_fp, fp_hca, linkage_fp, cutoff_fp = MASSAcluster.hca_clusters(
124
- fp_pca, names, 'FP', directoryfile_output, extension_type, linkage_method)
134
+ if large_library == True:
135
+ # First clustering (MiniBatch-KMeans):
136
+ # It performs KMeans clustering for the biological domain.
137
+ bio_hca = MASSAcluster.kmeans_clusters(
138
+ bio_pca, names, 'bio', directoryfile_output, extension_type)
139
+ # It performs KMeans clustering for the physicochemical domain.
140
+ phch_hca = MASSAcluster.kmeans_clusters(
141
+ phch_pca, names, 'PhCh', directoryfile_output, extension_type)
142
+ # It performs KMeans clustering for the structural domain.
143
+ fp_hca = MASSAcluster.kmeans_clusters(
144
+ fp_pca, names, 'FP', directoryfile_output, extension_type)
145
+ else:
146
+ # First clustering (HCA):
147
+ # It performs HCA clustering without generating the dendrogram for the biological domain.
148
+ leaves_cluster_bio, bio_hca, linkage_bio, cutoff_bio = MASSAcluster.hca_clusters(
149
+ bio_pca, names, 'bio', directoryfile_output, extension_type, linkage_method)
150
+ # It performs HCA clustering without generating the dendrogram for the physicochemical domain.
151
+ leaves_cluster_phch, phch_hca, linkage_phch, cutoff_phch = MASSAcluster.hca_clusters(
152
+ phch_pca, names, 'PhCh', directoryfile_output, extension_type, linkage_method)
153
+ # It performs HCA clustering without generating the dendrogram for the structural domain.
154
+ leaves_cluster_fp, fp_hca, linkage_fp, cutoff_fp = MASSAcluster.hca_clusters(
155
+ fp_pca, names, 'FP', directoryfile_output, extension_type, linkage_method)
125
156
 
126
157
  # It adds the biological cluster identification to the spreadsheet.
127
158
  dataframe = MASSApreparation.organize_df_clusterization(
@@ -137,7 +168,8 @@ def main():
137
168
  # It creates a matrix with cluster identifications for each of the three domains for Kmodes.
138
169
  matrix_for_kmodes = MASSApreparation.organize_for_kmodes(dataframe)
139
170
  # It performs Kmodes clustering for the general domain.
140
- all_hca = MASSAcluster.kmodes_clusters(matrix_for_kmodes, names)
171
+ all_hca = MASSAcluster.kmodes_clusters(matrix_for_kmodes, names,
172
+ directoryfile_output, extension_type)
141
173
  # It adds the general cluster identification to the spreadsheet.
142
174
  dataframe = MASSApreparation.organize_df_clusterization(
143
175
  dataframe, all_hca, 'all')
@@ -239,27 +271,30 @@ def main():
239
271
  MASSAsplit.log_of_distributions(
240
272
  bio_distr, bio_total, bio_training,
241
273
  bio_test, bio_val, splitting_strategy,
242
- writelog
274
+ write_log
243
275
  )
244
276
  phch_distr = 'Physicochemical Distribution'
245
277
  MASSAsplit.log_of_distributions(
246
278
  phch_distr, phch_total, phch_training,
247
279
  phch_test, phch_val, splitting_strategy,
248
- writelog
280
+ write_log
249
281
  )
250
282
  fp_distr = 'Structural (FP) Distribution'
251
283
  MASSAsplit.log_of_distributions(
252
284
  fp_distr, fp_total, fp_training,
253
285
  fp_test, fp_val, splitting_strategy,
254
- writelog
286
+ write_log
255
287
  )
256
288
  all_distr = 'General Distribution'
257
289
  MASSAsplit.log_of_distributions(
258
290
  all_distr, all_total, all_training,
259
291
  all_test, all_val, splitting_strategy,
260
- writelog
292
+ write_log
261
293
  )
262
- writelog.close()
294
+
295
+ # Output management:
296
+ # It adds, for each molecule, the values of the calculated properties, the identifications of each cluster and which set the molecule belongs to.
297
+ MASSAmoloutput.output_file_mols(dataframe, file_output)
263
298
 
264
299
  # Plot HCAs:
265
300
  if flag_dendrogram == True:
@@ -280,7 +315,8 @@ def main():
280
315
  extension_type, dendrogram_xfont_size, test_or_val_molecules
281
316
  ) # fp_Plot: Plot the HCA dendrogram
282
317
 
283
- # Output management:
284
- # It adds, for each molecule, the values of the calculated properties, the identifications of each cluster and which set the molecule belongs to.
285
- MASSAmoloutput.output_mols(dataframe, file_output)
286
- print('Completed')
318
+ end_time = time.time()
319
+ elapsed_time = end_time - start_time
320
+ write_log.write(f'Completed! Elapsed time = {elapsed_time} s.')
321
+ write_log.close()
322
+ print(f'Completed! Elapsed time = {elapsed_time} s.')
@@ -26,7 +26,7 @@ def capture_args():
26
26
  '[-n <number_of_principal_components>] [-v <svd_parameter>] [-l <linkage_method] '
27
27
  '[-t <extension_for_images>] [-d <dendrogram_font_size_on_Xaxis>] '
28
28
  '[-x <frequency_bar_plot_font_size_on_Xaxis>] [-f <toggle_dendrogram>] '
29
- '[-e <drop_molecules_with_errors]'
29
+ '[-e <drop_molecules_with_errors] [-a <large_dataset>]'
30
30
  )
31
31
  )
32
32
  required = parser.add_argument_group('required arguments')
@@ -120,9 +120,17 @@ def capture_args():
120
120
  )
121
121
  )
122
122
  optional.add_argument('-e', '--drop_errors', type=str, choices=['true', 'false'],
123
- metavar='DROP_MOLECULES_WITH_ERRORS', default='false', help=(
123
+ metavar='DROP_MOLECULES_WITH_ERRORS', default='true', help=(
124
124
  'Ignore chemistry errors, saving only molecules without any errors. '
125
- 'Default = \'false\'.'
125
+ 'False will treat molecule errors as fatal and fail the execution. '
126
+ 'Default = \'true\'.'
127
+ ))
128
+ optional.add_argument('-a', '--large_dataset', type=str, choices=['auto', 'true', 'false'],
129
+ metavar='LARGE_DATASET', default='auto', help=(
130
+ 'Switch the MASSA algorithm from HCA to KMeans to handle large datasets. '
131
+ 'Options = auto (switches to KMeans if ≥ 10,000 molecules), '
132
+ 'false = use HCA (for smaller datasets), '
133
+ 'true = use KMeans (for large datasets).'
126
134
  ))
127
135
  args = parser.parse_args()
128
136
 
@@ -153,6 +161,12 @@ def capture_args():
153
161
  flag_strategy = str(args.splitting_strategy).lower()
154
162
  # It captures whether or not to ignore chemistry errors.
155
163
  drop = str(args.drop_errors).lower()
164
+ # It captures whether or not is handling with a large dataset.
165
+ large_dataset = str(args.large_dataset).lower()
166
+ if large_dataset == 'true':
167
+ large_dataset = True
168
+ elif large_dataset == 'false':
169
+ large_dataset = False
156
170
 
157
171
  if args.the_biological_activities != None:
158
172
  # It creates a list with the names of the biological activities to be searched for.
@@ -210,5 +224,6 @@ def capture_args():
210
224
  linkage_method,
211
225
  flag_dendrogram,
212
226
  flag_strategy,
213
- drop
227
+ drop,
228
+ large_dataset
214
229
  )
@@ -3,6 +3,9 @@ from matplotlib import pyplot as plt
3
3
  from matplotlib import colors as colrs
4
4
  import scipy.cluster.hierarchy as sch
5
5
  from kmodes import kmodes
6
+ from sklearn.cluster import MiniBatchKMeans as KMeans
7
+ from multiprocessing import cpu_count as mpi_count
8
+ from scipy.spatial.distance import cdist
6
9
 
7
10
 
8
11
  def numerical_number_of_clusters(distances_list):
@@ -26,12 +29,15 @@ def numerical_number_of_clusters(distances_list):
26
29
  return distances.index(max(distances)) + 1
27
30
 
28
31
 
29
- def categorical_number_of_clusters(file):
32
+ def categorical_number_of_clusters(file, directory_fileoutput, extension_type):
30
33
  # Elbow method using the clustering cost:
31
34
  list_clus_costs = []
32
- for n in range(2, 21): # Calculate the cost for each n cluster.
35
+ maxcluster = 31
36
+ ran_curve = [x for x in range(2, maxcluster, 2)]
37
+ for n in range(2, maxcluster): # Calculate the cost for each n cluster.
33
38
  km = kmodes.KModes(n_clusters=n, verbose=0,
34
- n_init=20, init='Cao', max_iter=300)
39
+ n_init=20, init='Cao',
40
+ n_jobs=-1, max_iter=300)
35
41
  km.fit(X=file)
36
42
  # Costs = Clustering cost, defined as the sum distance of all points
37
43
  # to their respective cluster centroids.
@@ -39,10 +45,10 @@ def categorical_number_of_clusters(file):
39
45
  # Define the x-axis coordinates of the first and last points on the line.
40
46
  x1, y1 = 2, list_clus_costs[0]
41
47
  # Define the y-axis coordinates of the first and last points on the line.
42
- x2, y2 = 20, list_clus_costs[len(list_clus_costs)-1]
48
+ x2, y2 = 20, list_clus_costs[18]
43
49
 
44
50
  distances = []
45
- for i in range(len(list_clus_costs)):
51
+ for i in range(19):
46
52
  # Coordinate on the x-axis of the point to calculate the distance to the line.
47
53
  x0 = i+2
48
54
  # Coordinate on the y-axis of the point to calculate the distance to the line.
@@ -51,7 +57,52 @@ def categorical_number_of_clusters(file):
51
57
  denominator = ((y2 - y1)**2 + (x2 - x1)**2)**(1/2)
52
58
  # Distance between a point and a line.
53
59
  distances.append(numerator/denominator)
54
- return (distances.index(max(distances)) + 2)
60
+ number_of_clusters = (distances.index(max(distances)) + 2)
61
+ ident = "all"
62
+ plot_distortion(
63
+ list_clus_costs, directory_fileoutput,
64
+ ident, extension_type, number_of_clusters,
65
+ ran_curve)
66
+ return number_of_clusters
67
+
68
+
69
+ def kmeans_number_of_clusters(file, ident, directory_fileoutput, extension_type):
70
+ # Elbow method using the clustering cost:
71
+ list_clus_costs = []
72
+ maxcluster = 31
73
+ ran_curve = [x for x in range(2, maxcluster, 2)]
74
+ for n in range(2, maxcluster): # Calculate the cost for each n cluster.
75
+ km = KMeans(n_clusters=n, verbose=0,
76
+ init='k-means++', max_iter=300,
77
+ batch_size=256*int(mpi_count()),
78
+ compute_labels=True, random_state=2025)
79
+ km.fit(X=file)
80
+ # Costs = Clustering cost, defined as the sum distance of all points
81
+ # to their respective cluster centroids.
82
+ list_clus_costs.append(
83
+ np.sum(np.min(cdist(file, km.cluster_centers_, 'euclidean'), axis=1)**2) / file.shape[0])
84
+ # Define the x-axis coordinates of the first and last points on the line.
85
+ x1, y1 = 2, list_clus_costs[0]
86
+ # Define the y-axis coordinates of the first and last points on the line.
87
+ x2, y2 = 20, list_clus_costs[18]
88
+
89
+ distances = []
90
+ for i in range(19):
91
+ # Coordinate on the x-axis of the point to calculate the distance to the line.
92
+ x0 = i+2
93
+ # Coordinate on the y-axis of the point to calculate the distance to the line.
94
+ y0 = list_clus_costs[i]
95
+ numerator = abs((y2-y1)*x0 - (x2-x1)*y0 + x2*y1 - y2*x1)
96
+ denominator = ((y2 - y1)**2 + (x2 - x1)**2)**(1/2)
97
+ # Distance between a point and a line.
98
+ distances.append(numerator/denominator)
99
+
100
+ number_of_clusters = (distances.index(max(distances)) + 2)
101
+ plot_distortion(
102
+ list_clus_costs, directory_fileoutput,
103
+ ident, extension_type, number_of_clusters,
104
+ ran_curve)
105
+ return number_of_clusters
55
106
 
56
107
 
57
108
  def optimal_threshold(file, ident, directory_fileoutput, extension_type, linkage):
@@ -69,7 +120,7 @@ def optimal_threshold(file, ident, directory_fileoutput, extension_type, linkage
69
120
  if ident == 'bio':
70
121
  # Define the name, directory and extension of the Euclidean distance graph.
71
122
  name_euc_dist_file = directory_fileoutput + \
72
- '/Images/Distance_HCA_Images/Dist_HCA_biological.'+extension_type
123
+ 'Images/Distance_Images/Dist_HCA_biological.'+extension_type
73
124
  title_distances = 'Plot of Euclidean Distances from Biological HCA'
74
125
  print('Number of clusters:')
75
126
  # Print the number of clusters for the biological domain.
@@ -80,7 +131,7 @@ def optimal_threshold(file, ident, directory_fileoutput, extension_type, linkage
80
131
  elif ident == 'PhCh':
81
132
  # Define the name, directory and extension of the Euclidean distance graph.
82
133
  name_euc_dist_file = directory_fileoutput + \
83
- '/Images/Distance_HCA_Images/Dist_HCA_physicochemical.'+extension_type
134
+ 'Images/Distance_Images/Dist_HCA_physicochemical.'+extension_type
84
135
  title_distances = 'Plot of Euclidean Distances from Physicochemical HCA'
85
136
  # Print the number of clusters for the physicochemical domain.
86
137
  print('Physicochemical:', number_of_clusters)
@@ -90,7 +141,7 @@ def optimal_threshold(file, ident, directory_fileoutput, extension_type, linkage
90
141
  else:
91
142
  # Define the name, directory and extension of the Euclidean distance graph.
92
143
  name_euc_dist_file = directory_fileoutput + \
93
- '/Images/Distance_HCA_Images/Dist_HCA_structural.'+extension_type
144
+ 'Images/Distance_Images/Dist_HCA_structural.'+extension_type
94
145
  title_distances = 'Plot of Euclidean Distances from Structural HCA'
95
146
  # Print the number of clusters for the structural domain.
96
147
  print('Structural:', number_of_clusters)
@@ -279,24 +330,24 @@ def hca_plot(
279
330
  if ident == 'bio':
280
331
  plt.title('HCA of biological activity')
281
332
  plt.savefig(directory_fileoutput +
282
- '/Images/Dendrogram_HCA_Images/HCA_biological.'+extension_type)
333
+ 'Images/Dendrogram_HCA_Images/HCA_biological.'+extension_type)
283
334
  plt.close()
284
335
  elif ident == 'PhCh':
285
336
  plt.title('HCA of physicochemical properties')
286
337
  plt.savefig(directory_fileoutput +
287
- '/Images/Dendrogram_HCA_Images/HCA_physicochemical.'+extension_type)
338
+ 'Images/Dendrogram_HCA_Images/HCA_physicochemical.'+extension_type)
288
339
  plt.close()
289
340
  else:
290
341
  plt.title('HCA of AtomPairs fingerprint')
291
342
  plt.savefig(directory_fileoutput +
292
- '/Images/Dendrogram_HCA_Images/HCA_structural.'+extension_type)
343
+ 'Images/Dendrogram_HCA_Images/HCA_structural.'+extension_type)
293
344
  plt.close()
294
345
 
295
346
 
296
- def kmodes_clusters(file, names):
347
+ def kmodes_clusters(file, names, directory_fileoutput, extension_type):
297
348
  # Calculate the number of clusters for the Kmodes:
298
- n_clusters = categorical_number_of_clusters(file)
299
- print('General:', n_clusters) # Print the number of clusters.
349
+ n_clusters = categorical_number_of_clusters(
350
+ file, directory_fileoutput, extension_type)
300
351
 
301
352
  # Run Kmodes with the calculated number of clusters:
302
353
  # Parameter setting.
@@ -311,3 +362,114 @@ def kmodes_clusters(file, names):
311
362
  for i, a in zip(names, clusters_list):
312
363
  HCA_General_dict[i] = int(a+1)
313
364
  return HCA_General_dict
365
+
366
+
367
+ def plot_distortion(
368
+ list_clus_costs, directory_fileoutput,
369
+ ident, extension_type, number_of_clusters,
370
+ ran_curve):
371
+ plt.plot(
372
+ list(range(2, len(list_clus_costs)+2)),
373
+ list_clus_costs, '-o',
374
+ color='black',
375
+ markersize=4, linewidth=1,
376
+ markeredgewidth=1
377
+ ) # Initial setting of graph values and parameters.
378
+ if ident == 'bio':
379
+ # Define the name, directory and extension of the Distortion graph.
380
+ name_euc_dist_file = directory_fileoutput + \
381
+ 'Images/Distance_Images/Distortion_biological.'+extension_type
382
+ title_distances = 'Plot of Distortion from Biological Space'
383
+ print('Number of clusters:')
384
+ # Print the number of clusters for the biological domain.
385
+ print('Biological:', number_of_clusters)
386
+ plt.ylabel('KMeans Distortion') # Set the y-axis label.
387
+ elif ident == 'PhCh':
388
+ # Define the name, directory and extension of the Distortion graph.
389
+ name_euc_dist_file = directory_fileoutput + \
390
+ 'Images/Distance_Images/Distortion_physicochemical.'+extension_type
391
+ title_distances = 'Plot of Distortion from Physicochemical Space'
392
+ # Print the number of clusters for the physicochemical domain.
393
+ print('Physicochemical:', number_of_clusters)
394
+ plt.ylabel('KMeans Distortion') # Set the y-axis label.
395
+ elif ident == 'FP':
396
+ # Define the name, directory and extension of the Distortion graph.
397
+ name_euc_dist_file = directory_fileoutput + \
398
+ 'Images/Distance_Images/Distortion_structural.'+extension_type
399
+ title_distances = 'Plot of Distortion from Structural Space'
400
+ # Print the number of clusters for the structural domain.
401
+ print('Structural:', number_of_clusters)
402
+ plt.ylabel('KMeans Distortion') # Set the y-axis label.
403
+ else:
404
+ # Define the name, directory and extension of the Distortion graph.
405
+ name_euc_dist_file = directory_fileoutput + \
406
+ 'Images/Distance_Images/Cost_general.'+extension_type
407
+ title_distances = 'Cost Plot from General (KModes)'
408
+ # Print the number of clusters for the structural domain.
409
+ print('General:', number_of_clusters)
410
+ plt.ylabel('KModes Cost') # Set the y-axis label.
411
+ plt.title(title_distances) # Define the graph title.
412
+ plt.xticks(ran_curve) # Define the x-axis ticks.
413
+ plt.savefig(name_euc_dist_file) # Save figure.
414
+ plt.close() # Close plot.
415
+
416
+
417
+ def kmeans_number_of_clusters(file, ident, directory_fileoutput, extension_type):
418
+ # Elbow method using the clustering cost:
419
+ list_clus_costs = []
420
+ maxcluster = 31
421
+ ran_curve = [x for x in range(2, maxcluster, 2)]
422
+ for n in range(2, maxcluster): # Calculate the cost for each n cluster.
423
+ km = KMeans(n_clusters=n, verbose=0,
424
+ init='k-means++', max_iter=300,
425
+ batch_size=256*int(mpi_count()),
426
+ compute_labels=True, random_state=2025)
427
+ km.fit(X=file)
428
+ # Costs = Clustering cost, defined as the sum distance of all points
429
+ # to their respective cluster centroids.
430
+ list_clus_costs.append(
431
+ np.sum(np.min(cdist(file, km.cluster_centers_, 'euclidean'), axis=1)**2) / file.shape[0])
432
+ # Define the x-axis coordinates of the first and last points on the line.
433
+ x1, y1 = 2, list_clus_costs[0]
434
+ # Define the y-axis coordinates of the first and last points on the line.
435
+ x2, y2 = 20, list_clus_costs[18]
436
+
437
+ distances = []
438
+ for i in range(19):
439
+ # Coordinate on the x-axis of the point to calculate the distance to the line.
440
+ x0 = i+2
441
+ # Coordinate on the y-axis of the point to calculate the distance to the line.
442
+ y0 = list_clus_costs[i]
443
+ numerator = abs((y2-y1)*x0 - (x2-x1)*y0 + x2*y1 - y2*x1)
444
+ denominator = ((y2 - y1)**2 + (x2 - x1)**2)**(1/2)
445
+ # Distance between a point and a line.
446
+ distances.append(numerator/denominator)
447
+
448
+ number_of_clusters = (distances.index(max(distances)) + 2)
449
+ plot_distortion(
450
+ list_clus_costs, directory_fileoutput,
451
+ ident, extension_type, number_of_clusters,
452
+ ran_curve)
453
+ return number_of_clusters
454
+
455
+
456
+ def kmeans_clusters(file, names, ident, directory_fileoutput, extension_type):
457
+ # Calculate the number of clusters for the Kmodes:
458
+ n_clusters = kmeans_number_of_clusters(
459
+ file, ident, directory_fileoutput, extension_type)
460
+
461
+ # Run Kmodes with the calculated number of clusters:
462
+ # Parameter setting.
463
+ km = KMeans(n_clusters=n_clusters, verbose=0,
464
+ init='k-means++', max_iter=300,
465
+ batch_size=256*int(mpi_count()),
466
+ compute_labels=True, random_state=2025)
467
+ # Run Kmodes for the dataset and return an array with cluster labels for each molecule.
468
+ clusters = km.fit_predict(file)
469
+ clusters_list = list(clusters) # Transform np.array into a list.
470
+
471
+ # Create a dictionary with the pair "molecule name": "General cluster ID":
472
+ kmeans_cluster_dict = {}
473
+ for i, a in zip(names, clusters_list):
474
+ kmeans_cluster_dict[i] = int(a+1)
475
+ return kmeans_cluster_dict
@@ -5,11 +5,11 @@ from rdkit.Chem import AllChem
5
5
  def name_extraction(file):
6
6
  names = []
7
7
  dict_names = {}
8
- for i in file:
8
+ for idx, f in enumerate(file):
9
9
  try:
10
- names.append(i.GetProp('_Name')) # Get name of molecules to a list
10
+ names.append(f.GetProp('_Name')) # Get name of molecules to a list
11
11
  # Get name of molecules to a dict
12
- dict_names[i.GetProp('_Name')] = i
12
+ dict_names[idx] = [f.GetProp('_Name'), f]
13
13
  except:
14
14
  names.append(None)
15
15
  if None in names: # If any molecule is unnamed it returns an error.
@@ -17,8 +17,39 @@ def name_extraction(file):
17
17
  print('Closing...')
18
18
  exit()
19
19
  else:
20
- dataframe = pd.DataFrame({'molecules': pd.Series(dict_names)})
21
- return names, dataframe
20
+ # Dealing with duplicates:
21
+ duplicates = {}
22
+ for idx, value in dict_names.items():
23
+ name = value[0]
24
+ mol = value[1]
25
+ if name in duplicates.keys():
26
+ duplicates[name].append([idx, mol])
27
+ else:
28
+ duplicates[name] = [[idx, mol]]
29
+
30
+ final_names = []
31
+ final_names_dict = {}
32
+ for k, v in duplicates.items():
33
+ name = k
34
+ replicates = v
35
+ if len(replicates) > 1:
36
+ for r in replicates:
37
+ index = r[0]
38
+ molecule = r[1]
39
+ final_key = f"{name}_idx-_{index}"
40
+ molecule.SetProp("_Name", final_key)
41
+ final_names_dict[final_key] = molecule
42
+ final_names.append(final_key)
43
+ else:
44
+ for r in replicates:
45
+ index = r[0]
46
+ molecule = r[1]
47
+ final_key = name
48
+ molecule.SetProp("_Name", final_key)
49
+ final_names_dict[final_key] = molecule
50
+ final_names.append(final_key)
51
+ dataframe = pd.DataFrame({'molecules': pd.Series(final_names_dict)})
52
+ return final_names, dataframe
22
53
 
23
54
 
24
55
  def the_biological_handler(sdf_property_names, numberBioAct, BioActAsArgs):
@@ -20,7 +20,7 @@ def initial_print():
20
20
  ██░░░▓▓▓▓░░░░░░░▓▓▓▓░░░░░░░▓▓▓▓░░░░█
21
21
  ██░░░░▓▓▓▓▓▓░░░░░▓▓▓▓▓▓░░░░░▓▓▓▓▓▓░░░░█ MASSA Algorithm:
22
22
  ████░░░░░▓▓▓▓░░░░░░░▓▓▓▓░░░░░░░▓▓▓▓░░░░░█████ Molecular data set sampling — Training-Test Separation
23
- █▓▓▓███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███▓▓▓█ Version: v. 1.0.0
23
+ █▓▓▓███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███▓▓▓█ Version: v. 2.0.1
24
24
  █▒▓▓▒▒▒████░░░░░░░░░░░░░░░░░░░░░░░░░░░░████▒▒▓▓▒█
25
25
  ██▓▒▒▒▓▓▓▓██████████████████████████████▓▓▓▒▒▒▒▓█ Developed by: Gabriel Corrêa Veríssimo
26
26
  ██▒▒▒▓▓▒▒▒▒▓▓▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▓▓▒▒▒▓▓▒▒██ LICENSE: AGPL-3.0
@@ -28,3 +28,7 @@ def initial_print():
28
28
  ███▓▓▓▓▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▓▓████
29
29
  ███████████████████████████████████
30
30
  ''')
31
+
32
+
33
+ def python_logo():
34
+ print("🍕 MASSA Algorithm v. 2.0.1: Molecular data set sampling")
@@ -1,7 +1,7 @@
1
1
  from rdkit.Chem import AllChem
2
2
 
3
3
 
4
- def output_mols(dataframe, file_output):
4
+ def output_mols(dataframe):
5
5
  """
6
6
  For each molecule, adds the values of the calculated properties,
7
7
  the identifications of each cluster and which set the molecule belongs to.
@@ -9,7 +9,6 @@ def output_mols(dataframe, file_output):
9
9
 
10
10
  Args:
11
11
  dataframe (pd.DataFrame): The dataframe of molecules.
12
- file_output (str): The path to the output file.
13
12
  """
14
13
  # Creation of dictionaries with the name of the molecule:
15
14
  dict_molecules = dict(
@@ -41,6 +40,8 @@ def output_mols(dataframe, file_output):
41
40
 
42
41
  # Add the properties to molecules from the name:molecule dictionary:
43
42
  for i in dict_molecules.keys():
43
+ name = dict_molecules[i].GetProp('_Name')
44
+ dict_molecules[i].SetProp("_Name", name)
44
45
  dict_molecules[i].SetProp('Set', str(dict_set[i]))
45
46
  dict_molecules[i].SetProp('HBA count', str(dic_hba[i]))
46
47
  dict_molecules[i].SetProp('HBD count', str(dic_hbd[i]))
@@ -62,9 +63,27 @@ def output_mols(dataframe, file_output):
62
63
 
63
64
  # Create a dict_values with just the molecules
64
65
  # (the name of the molecule is already part of molecule object):
65
- output_mols = dict_molecules.values()
66
+ out_mols = list(dict_molecules.values())
67
+ return out_mols
68
+
69
+
70
+ def output_file_mols(dataframe, file_output):
71
+ """
72
+ For each molecule, adds the values of the calculated properties,
73
+ the identifications of each cluster and which set the molecule belongs to.
74
+ Also, it creates the final output .sdf file.
75
+
76
+ Args:
77
+ dataframe (pd.DataFrame): The dataframe of molecules.
78
+ file_output (str): The path to the output file.
79
+ """
80
+
81
+ # Recovers a list with just the molecules
82
+ # (the name of the molecule is already part of molecule object):
83
+ out_mols = output_mols(dataframe)
66
84
 
67
85
  # Create output ".sdf" file:
68
86
  with AllChem.SDWriter(file_output) as w:
69
- for m in output_mols:
87
+ for m in out_mols:
70
88
  w.write(m)
89
+ return out_mols
@@ -1,7 +1,7 @@
1
1
  import os
2
2
 
3
3
 
4
- def output_directory(directory_file_output):
4
+ def output_directory(directory_file_output, flag_dendrogram):
5
5
  """
6
6
  Creates the output directories.
7
7
  """
@@ -10,13 +10,14 @@ def output_directory(directory_file_output):
10
10
  except FileExistsError:
11
11
  pass
12
12
  try:
13
- os.makedirs(directory_file_output+'Images/Distance_HCA_Images')
14
- except FileExistsError:
15
- pass
16
- try:
17
- os.makedirs(directory_file_output+'Images/Dendrogram_HCA_Images')
13
+ os.makedirs(directory_file_output+'Images/Distance_Images')
18
14
  except FileExistsError:
19
15
  pass
16
+ if flag_dendrogram == True:
17
+ try:
18
+ os.makedirs(directory_file_output+'Images/Dendrogram_HCA_Images')
19
+ except FileExistsError:
20
+ pass
20
21
  try:
21
22
  os.makedirs(directory_file_output+'Images/Distribution')
22
23
  except FileExistsError:
@@ -50,33 +50,33 @@ def find_name_column(columns):
50
50
  Returns:
51
51
  column_name (str): returns the name of the molecule name column.
52
52
  """
53
-
53
+ name_column = ""
54
54
  for column in columns:
55
55
  if "molecule name" == column.lower():
56
56
  print((f"Found '{column}' column."
57
57
  " Using it to name the molecules."))
58
58
  name_column = column
59
- return name_column
59
+ break
60
60
  elif "name" == column.lower():
61
61
  print((f"Found '{column}' column."
62
62
  " Using it to name the molecules."))
63
63
  name_column = column
64
- return name_column
64
+ break
65
65
  elif "molecule chembl id" == column.lower():
66
66
  print((f"Found '{column}' column."
67
67
  " Using it to name the molecules."))
68
68
  name_column = column
69
- return name_column
69
+ break
70
70
  elif "id" == column.lower():
71
71
  print((f"Found '{column}' column."
72
72
  " Using it to name the molecules."))
73
73
  name_column = column
74
- return name_column
75
- else:
76
- print(("Unable to find a column with molecule names."
77
- " Using the index to name the molecules."))
78
- name_column = "index"
79
- return name_column
74
+ break
75
+ if name_column == "":
76
+ print(("Unable to find a column with molecule names."
77
+ " Using the index to name the molecules."))
78
+ name_column = "index"
79
+ return name_column
80
80
 
81
81
 
82
82
  def read_df_smiles(df, write_log, drop_errors):
@@ -94,7 +94,6 @@ def read_df_smiles(df, write_log, drop_errors):
94
94
  smi_name_column = find_smiles_column(columns)
95
95
  name_name_column = find_name_column(columns)
96
96
 
97
- # name_name_column = "index"
98
97
  columns.remove(smi_name_column)
99
98
  columns.remove(name_name_column)
100
99
 
@@ -132,13 +131,13 @@ def read_df_smiles(df, write_log, drop_errors):
132
131
  return mols
133
132
 
134
133
 
135
- def read_EXCEL_smiles(file, write_log):
134
+ def read_EXCEL_smiles(file, write_log, drop_errors):
136
135
  df_supplier = pd.read_excel(file)
137
136
  mols_sanitized = read_df_smiles(df_supplier, write_log, drop_errors)
138
137
  return mols_sanitized
139
138
 
140
139
 
141
- def read_CSV_smiles(file, write_log):
140
+ def read_CSV_smiles(file, write_log, drop_errors):
142
141
  df_supplier = pd.read_csv(file, sep=",")
143
142
  mols_sanitized = read_df_smiles(df_supplier, write_log, drop_errors)
144
143
  return mols_sanitized
@@ -281,18 +280,18 @@ def read_molecules(file, write_log, drop_errors):
281
280
  elif file.endswith(".sdf") or file.endswith(".mol"):
282
281
  molecules = read_SDF(file, write_log, drop_errors)
283
282
  elif file.endswith(".xlsx") or file.endswith(".xls"):
284
- molecules = read_EXCEL_smiles(file, write_log)
283
+ molecules = read_EXCEL_smiles(file, write_log, drop_errors)
285
284
  elif file.endswith(".csv"):
286
- molecules = read_CSV_smiles(file, write_log)
285
+ molecules = read_CSV_smiles(file, write_log, drop_errors)
287
286
  return molecules
288
287
 
289
288
 
290
289
  def hydrogen_add(file):
291
290
  molsH = []
292
291
  for i in file:
292
+ # Add hydrogens keeping 3D coordenates.
293
293
  i = AllChem.AddHs(i, addCoords=True)
294
294
  molsH.append(i)
295
- # Add hydrogens keeping 3D coordenates.
296
295
  return molsH
297
296
 
298
297
 
@@ -46,16 +46,13 @@ def pca_maker(file, nPCS, svd_parameter):
46
46
  def organize_df_clusterization(file, HCAdict, ident):
47
47
  if ident == 'all':
48
48
  file['Cluster_General'] = pd.Series(HCAdict)
49
- return file
50
49
  elif ident == 'bio':
51
50
  file['Cluster_Biological'] = pd.Series(HCAdict)
52
- return file
53
51
  elif ident == 'PhCh':
54
52
  file['Cluster_Physicochemical'] = pd.Series(HCAdict)
55
- return file
56
53
  else:
57
54
  file['Cluster_Structural'] = pd.Series(HCAdict)
58
- return file
55
+ return file
59
56
 
60
57
 
61
58
  # Create a matrix with cluster identifications for each of the three domains, in order to prepare for Kmodes.
@@ -19,6 +19,7 @@ def split_train_test_sets(file, training_percent, splitting_strategy):
19
19
  test_or_val_molecules: An identification of which molecules are on the validation set.
20
20
 
21
21
  """
22
+ file["start_index"] = range(len(file))
22
23
  if splitting_strategy == "tt":
23
24
  # Split the set of molecules into training set and test set,
24
25
  # preserving the cluster diversity of the total set according
@@ -68,6 +69,7 @@ def split_train_test_sets(file, training_percent, splitting_strategy):
68
69
  dataset = pd.concat([training, test, validation])
69
70
  # Define the output sets for the algorithm.
70
71
  test_or_val_molecules = [test_molecules, validation_molecules]
72
+ dataset = dataset.sort_values(by="start_index")
71
73
  return dataset, test_or_val_molecules
72
74
 
73
75
 
@@ -0,0 +1,353 @@
1
+ from MASSA_Algorithm import MASSAlogos
2
+ from MASSA_Algorithm import MASSAargs
3
+ from MASSA_Algorithm import MASSAod
4
+ from MASSA_Algorithm import MASSAopen_files
5
+ from MASSA_Algorithm import MASSAextraction
6
+ from MASSA_Algorithm import MASSAdescriptors
7
+ from MASSA_Algorithm import MASSApreparation
8
+ from MASSA_Algorithm import MASSAcluster
9
+ from MASSA_Algorithm import MASSAsplit
10
+ from MASSA_Algorithm import MASSAmoloutput
11
+ import time
12
+
13
+
14
+ def returns_zero(total, test, validation):
15
+ """
16
+ Description:
17
+ It evaluates whether the distribution is inadequate. That is,
18
+ when the iterated cluster has a percentage greater than 0.5%
19
+ in the complete dataset but less than 0.5% in the test set.
20
+
21
+ Args:
22
+ total (dict):
23
+ Percentage of molecules in each cluster for the whole dataset.
24
+
25
+ test (dict):
26
+ Percentage of molecules in each cluster for the test subset.
27
+
28
+ validation (dict):
29
+ Percentage of molecules in each cluster for the validation subset.
30
+
31
+ Returns:
32
+ definer (boolean): Returns True if the distribution is
33
+ inadequate, and False if it is adequate.
34
+ """
35
+ definer = False
36
+ for i in total.keys():
37
+ if (total[i] > 0.5) and (test[i] <= 0.5):
38
+ # Definer = True (Distribution was not done properly).
39
+ definer = True
40
+ elif validation != None:
41
+ if (total[i] > 0.5) and (validation[i] <= 0.5):
42
+ definer = True
43
+ return definer
44
+
45
+
46
+ def py_massa(
47
+ file_input=str(),
48
+ working_directory=str(),
49
+ job_name="MASSAoutput.sdf",
50
+ extension_type="svg",
51
+ dendrogram_xfont_size=5,
52
+ barplot_xfont_size=9,
53
+ training_percent=round(float(0.8), 3),
54
+ bioactivities_as_args=None,
55
+ npcs=0.85,
56
+ svd_parameter="full",
57
+ linkage_method="complete",
58
+ flag_dendrogram=True,
59
+ splitting_strategy="tt",
60
+ drop_errors=True,
61
+ large_library="auto"
62
+ ):
63
+ # Initializing from Python.
64
+ # Print the program logo:
65
+ MASSAlogos.python_logo()
66
+ start_time = time.time()
67
+
68
+ # Defining the working_directory alias:
69
+ working_directory = f"{working_directory}/"
70
+ directoryfile_output = working_directory
71
+
72
+ # It creates the output directories:
73
+ MASSAod.output_directory(directoryfile_output, flag_dendrogram)
74
+
75
+ # Test nPCs:
76
+ if npcs > 1:
77
+ npcs = int(npcs)
78
+ elif (npcs > 0) and (npcs <= 1):
79
+ npcs = float(npcs)
80
+ else:
81
+ raise ValueError(
82
+ f"ERROR: The defined number of main components is invalid. npcs = {str(npcs)}")
83
+
84
+ # Autodetect if the biological activities are set.
85
+ if isinstance(bioactivities_as_args, str):
86
+ number_bioact = 1
87
+ bioactivities_as_args = [bioactivities_as_args]
88
+ elif isinstance(bioactivities_as_args, list):
89
+ number_bioact = len(bioactivities_as_args)
90
+ elif bioactivities_as_args == None:
91
+ raise ValueError(
92
+ "The biological activities are not specified. bioactivities_as_args == None")
93
+
94
+ # Create log.txt file in directory:
95
+ log_path = directoryfile_output+'log.txt'
96
+ write_log = open(log_path, 'w')
97
+
98
+ if isinstance(file_input, str):
99
+ # Initial file management:
100
+ # Read molecules.
101
+ mols = MASSAopen_files.read_molecules(
102
+ file_input, write_log, drop_errors)
103
+ else:
104
+ mols = file_input
105
+
106
+ # Test if large_library is set:
107
+ if isinstance(large_library, str):
108
+ if large_library.lower() == 'true':
109
+ large_library = True
110
+ elif large_library.lower() == 'false':
111
+ large_library = False
112
+ elif large_library.lower() == 'auto':
113
+ if int(len(mols)) > 9999:
114
+ large_library = True
115
+ else:
116
+ large_library = False
117
+
118
+ # If auto detection is enable to select the initial cluster algorithm.
119
+ if large_library == 'auto':
120
+ if int(len(mols)) > 9999:
121
+ large_library = True
122
+ else:
123
+ large_library = False
124
+
125
+ if large_library == True:
126
+ # Deactivates the HCA dendrogram.
127
+ flag_dendrogram = False
128
+
129
+ # Extracting the property names from the ".sdf" input file.
130
+ sdf_property_names = MASSAopen_files.get_sdf_property_names(mols)
131
+ # Structure 3D management - It adds hydrogens keeping 3D coordenates.
132
+ mols_h = MASSAopen_files.hydrogen_add(mols)
133
+
134
+ # Extraction properties from ".sdf":
135
+ # It extracts the names of the molecules and creates a name:molecule dictionary and a dataframe.
136
+ names, dataframe = MASSAextraction.name_extraction(mols_h)
137
+
138
+ # It defines a list of what biological activities are being extracted.
139
+ biological_activity = MASSAextraction.the_biological_handler(
140
+ sdf_property_names, number_bioact, bioactivities_as_args)
141
+ # It adds the biological activities to the dataframe.
142
+ dataframe = MASSAextraction.list_activities(dataframe, biological_activity)
143
+
144
+ # Get fingeprint and other descriptors:
145
+ # Get physicochemical descriptors.
146
+ dataframe = MASSAdescriptors.physicochemical_descriptors(dataframe)
147
+ # Get AtomPairs fingerprint.
148
+ dataframe = MASSAdescriptors.atompairs_fingerprint(dataframe)
149
+
150
+ # Normalizes physicochemical and biological properties.
151
+ # Creates matrices for the three domains (struc, phys-chem, bio):
152
+ bio_matrix, phch_matrix, fp_matrix = MASSApreparation.normalizer_or_matrix(
153
+ dataframe, biological_activity)
154
+
155
+ # PCA:
156
+ # PCA for the biological domain.
157
+ bio_pca = MASSApreparation.pca_maker(bio_matrix, npcs, svd_parameter)
158
+ # PCA for the physicochemical domain.
159
+ phch_pca = MASSApreparation.pca_maker(phch_matrix, npcs, svd_parameter)
160
+ # PCA for the structural domain.
161
+ fp_pca = MASSApreparation.pca_maker(fp_matrix, npcs, svd_parameter)
162
+
163
+ if large_library == True:
164
+ # First clustering (MiniBatch-KMeans):
165
+ # It performs KMeans clustering for the biological domain.
166
+ bio_hca = MASSAcluster.kmeans_clusters(
167
+ bio_pca, names, 'bio', directoryfile_output, extension_type)
168
+ # It performs KMeans clustering for the physicochemical domain.
169
+ phch_hca = MASSAcluster.kmeans_clusters(
170
+ phch_pca, names, 'PhCh', directoryfile_output, extension_type)
171
+ # It performs KMeans clustering for the structural domain.
172
+ fp_hca = MASSAcluster.kmeans_clusters(
173
+ fp_pca, names, 'FP', directoryfile_output, extension_type)
174
+ else:
175
+ # First clustering (HCA):
176
+ # It performs HCA clustering without generating the dendrogram for the biological domain.
177
+ leaves_cluster_bio, bio_hca, linkage_bio, cutoff_bio = MASSAcluster.hca_clusters(
178
+ bio_pca, names, 'bio', directoryfile_output, extension_type, linkage_method)
179
+ # It performs HCA clustering without generating the dendrogram for the physicochemical domain.
180
+ leaves_cluster_phch, phch_hca, linkage_phch, cutoff_phch = MASSAcluster.hca_clusters(
181
+ phch_pca, names, 'PhCh', directoryfile_output, extension_type, linkage_method)
182
+ # It performs HCA clustering without generating the dendrogram for the structural domain.
183
+ leaves_cluster_fp, fp_hca, linkage_fp, cutoff_fp = MASSAcluster.hca_clusters(
184
+ fp_pca, names, 'FP', directoryfile_output, extension_type, linkage_method)
185
+
186
+ # It adds the biological cluster identification to the spreadsheet.
187
+ dataframe = MASSApreparation.organize_df_clusterization(
188
+ dataframe, bio_hca, 'bio')
189
+ # It adds the physicochemical cluster identification to the spreadsheet.
190
+ dataframe = MASSApreparation.organize_df_clusterization(
191
+ dataframe, phch_hca, 'PhCh')
192
+ # It adds the structural cluster identification to the spreadsheet.
193
+ dataframe = MASSApreparation.organize_df_clusterization(
194
+ dataframe, fp_hca, 'FP')
195
+
196
+ # Second clustering (Kmodes):
197
+ # It creates a matrix with cluster identifications for each of the three domains for Kmodes.
198
+ matrix_for_kmodes = MASSApreparation.organize_for_kmodes(dataframe)
199
+ # It performs Kmodes clustering for the general domain.
200
+ all_hca = MASSAcluster.kmodes_clusters(matrix_for_kmodes, names,
201
+ directoryfile_output, extension_type)
202
+ # It adds the general cluster identification to the spreadsheet.
203
+ dataframe = MASSApreparation.organize_df_clusterization(
204
+ dataframe, all_hca, 'all')
205
+
206
+ # Split into training, test:
207
+ dataframe, test_or_val_molecules = MASSAsplit.split_train_test_sets(
208
+ dataframe, training_percent, splitting_strategy)
209
+
210
+ # Bar plot of frequencies
211
+ # (Calculates the percentages of molecules in each cluster for each
212
+ # dataset and generates a bar graph for each domain):
213
+
214
+ # Biological Bar Plot
215
+ bio_total, bio_training, bio_test, bio_val = MASSAsplit.freq_clusters(
216
+ dataframe,
217
+ directoryfile_output,
218
+ extension_type,
219
+ 'Cluster_Biological',
220
+ barplot_xfont_size,
221
+ splitting_strategy)
222
+ # Physicochemical Bar Plot
223
+ phch_total, phch_training, phch_test, phch_val = MASSAsplit.freq_clusters(
224
+ dataframe,
225
+ directoryfile_output,
226
+ extension_type,
227
+ 'Cluster_Physicochemical',
228
+ barplot_xfont_size,
229
+ splitting_strategy)
230
+ # Structural Bar Plot
231
+ fp_total, fp_training, fp_test, fp_val = MASSAsplit.freq_clusters(
232
+ dataframe,
233
+ directoryfile_output,
234
+ extension_type,
235
+ 'Cluster_Structural',
236
+ barplot_xfont_size,
237
+ splitting_strategy)
238
+ # General Bar Plot
239
+ all_total, all_training, all_test, all_val = MASSAsplit.freq_clusters(
240
+ dataframe,
241
+ directoryfile_output,
242
+ extension_type,
243
+ 'Cluster_General',
244
+ barplot_xfont_size,
245
+ splitting_strategy)
246
+
247
+ # Verifying percentages:
248
+ # For biological domain:
249
+ bio_ok = returns_zero(bio_total, bio_test, bio_val)
250
+ # For physicochemical domain:
251
+ phch_ok = returns_zero(phch_total, phch_test, phch_val)
252
+ # For structural domain:
253
+ fp_ok = returns_zero(fp_total, fp_test, fp_val)
254
+ ok = [bio_ok, phch_ok, fp_ok]
255
+ max_iters = 0
256
+
257
+ # Redo the distribution in case of errors (up to 10 times):
258
+ while (True in ok) and (max_iters < 10):
259
+ # Split into training, test:
260
+ dataframe, test_or_val_molecules = MASSAsplit.split_train_test_sets(
261
+ dataframe, training_percent, splitting_strategy)
262
+
263
+ # Bar plot of frequencies (Calculates the percentages of molecules in each
264
+ # cluster for each dataset and generates a bar graph for each domain):
265
+
266
+ # Biological Bar Plot:
267
+ bio_total, bio_training, bio_test, bio_val = MASSAsplit.freq_clusters(
268
+ dataframe, directoryfile_output, extension_type,
269
+ 'Cluster_Biological', barplot_xfont_size, splitting_strategy
270
+ )
271
+ # Physicochemical Bar Plot
272
+ phch_total, phch_training, phch_test, phch_val = MASSAsplit.freq_clusters(
273
+ dataframe, directoryfile_output, extension_type,
274
+ 'Cluster_Physicochemical', barplot_xfont_size, splitting_strategy
275
+ )
276
+ # Structural Bar Plot
277
+ fp_total, fp_training, fp_test, fp_val = MASSAsplit.freq_clusters(
278
+ dataframe, directoryfile_output, extension_type,
279
+ 'Cluster_Structural', barplot_xfont_size, splitting_strategy
280
+ )
281
+ # General Bar Plot
282
+ all_total, all_training, all_test, all_val = MASSAsplit.freq_clusters(
283
+ dataframe, directoryfile_output, extension_type,
284
+ 'Cluster_General', barplot_xfont_size, splitting_strategy
285
+ )
286
+
287
+ # Verifying percentages:
288
+
289
+ # For biological domain:
290
+ bio_ok = returns_zero(bio_total, bio_test, bio_val)
291
+ # For physicochemical domain:
292
+ phch_ok = returns_zero(phch_total, phch_test, phch_val)
293
+ # For structural domain:
294
+ fp_ok = returns_zero(fp_total, fp_test, fp_val)
295
+ ok = [bio_ok, phch_ok, fp_ok]
296
+ max_iters += 1
297
+
298
+ # Write distribution information to log file:
299
+ bio_distr = 'Biological Distribution'
300
+ MASSAsplit.log_of_distributions(
301
+ bio_distr, bio_total, bio_training,
302
+ bio_test, bio_val, splitting_strategy,
303
+ write_log
304
+ )
305
+ phch_distr = 'Physicochemical Distribution'
306
+ MASSAsplit.log_of_distributions(
307
+ phch_distr, phch_total, phch_training,
308
+ phch_test, phch_val, splitting_strategy,
309
+ write_log
310
+ )
311
+ fp_distr = 'Structural (FP) Distribution'
312
+ MASSAsplit.log_of_distributions(
313
+ fp_distr, fp_total, fp_training,
314
+ fp_test, fp_val, splitting_strategy,
315
+ write_log
316
+ )
317
+ all_distr = 'General Distribution'
318
+ MASSAsplit.log_of_distributions(
319
+ all_distr, all_total, all_training,
320
+ all_test, all_val, splitting_strategy,
321
+ write_log
322
+ )
323
+
324
+ # Output management:
325
+ # It adds, for each molecule, the values of the calculated properties, the identifications of each cluster and which set the molecule belongs to.
326
+ out_name = working_directory + job_name
327
+ out_mols = MASSAmoloutput.output_file_mols(dataframe, out_name)
328
+
329
+ # Plot HCAs:
330
+ if flag_dendrogram == True:
331
+ print('\nGenerating dendrogram images. Please wait...')
332
+ MASSAcluster.hca_plot(
333
+ linkage_bio, names, leaves_cluster_bio,
334
+ cutoff_bio, 'bio', directoryfile_output,
335
+ extension_type, dendrogram_xfont_size, test_or_val_molecules
336
+ ) # Bio_Plot: Plot the HCA dendrogram
337
+ MASSAcluster.hca_plot(
338
+ linkage_phch, names, leaves_cluster_phch,
339
+ cutoff_phch, 'PhCh', directoryfile_output,
340
+ extension_type, dendrogram_xfont_size, test_or_val_molecules
341
+ ) # phch_Plot: Plot the HCA dendrogram
342
+ MASSAcluster.hca_plot(
343
+ linkage_fp, names, leaves_cluster_fp,
344
+ cutoff_fp, 'FP', directoryfile_output,
345
+ extension_type, dendrogram_xfont_size, test_or_val_molecules
346
+ ) # fp_Plot: Plot the HCA dendrogram
347
+
348
+ end_time = time.time()
349
+ elapsed_time = end_time - start_time
350
+ write_log.write(f'Completed! Elapsed time = {elapsed_time} s.')
351
+ write_log.close()
352
+ print(f'Completed! Elapsed time = {elapsed_time} s.')
353
+ return out_mols
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
- Name: MASSA-Algorithm
3
- Version: 1.0.0
2
+ Name: MASSA_Algorithm
3
+ Version: 2.0.1
4
4
  Summary: MASSA Algorithm is a Python package to separate data sets of molecules into training and test sets, considering the diversity of structural, physicochemical and biological characteristics of these molecules.
5
5
  Home-page: https://github.com/gcverissimo/MASSA_Algorithm
6
6
  Author: Gabriel Corrêa Veríssimo
@@ -43,7 +43,16 @@ MASSA Algorithm: A tool for separating data sets of molecules into training and
43
43
 
44
44
 
45
45
  ## Version 1.0.0
46
- Version 1.0.0 includes a code refactoring and adds the ability to split data into training, test, and validation subsets.
46
+ - Version 1.0.0 includes a code refactoring and adds the ability to split data into training, test, and validation subsets.
47
+
48
+ ## Version 2.0.0
49
+ - Version 2.0.0 introduces algorithm changes to better handle large datasets. Specifically, for datasets with 10,000 molecules or more, Hierarchical Clustering Analysis (HCA) is automatically replaced by MiniBatch K-Means. This is the new default behavior, but it can be controlled using the flag -a / --large-datasets. Since dendrograms are generated from HCA, dendrogram plotting is disabled for large datasets. In such cases, the flag -f / --dendrogram-plot is automatically disabled, regardless of user input.
50
+
51
+ - This version introduces the ability to call the MASSA Algorithm from any Python script using MASSA_Algorithm.pyMASSA.py_massa(). However, instructions and documentation for doing so will be provided in future updates.
52
+
53
+ - Version 2.0.0 also addresses the "NaN/None values in y" error, which was caused by duplicate molecule names. In this version, molecules with identical names are detected and renamed by appending their original index as a suffix to ensure uniqueness.
54
+
55
+ - MASSA now preserves the input order of molecules in its output. However, molecules with chemical errors are skipped and excluded from the process.
47
56
 
48
57
  ## Instalation
49
58
  MASSA Algorithm can be installed using pip:
@@ -69,6 +78,7 @@ python setup.py install
69
78
  * scipy: >= 1.6;
70
79
  * scikit-learn: > 0.24;
71
80
  * kmodes:¹ >= 0.10.
81
+
72
82
  Newer versions of the packages may also work, but they need to be tested.
73
83
  NOTE: Also tested on: scikit-learn: 1.7.0, scipy: 1.16.1, numpy: 2.0.2, rdkit: 2025.03.5.
74
84
 
@@ -135,7 +145,11 @@ Optional arguments include:
135
145
  * **Ignore Errors**: ```-e``` or ```--drop_errors```.
136
146
  * Ignore chemistry errors, saving only molecules without any errors.
137
147
  * Options = true (ignore molecule errors and log them in the log file), false (treat molecule errors as fatal and fail the execution).
138
- * Default = false.
148
+ * Default = true.
149
+ * **Large Dataset**: ```-a``` or ```--large_dataset```.
150
+ * Switch the MASSA algorithm from HCA to MiniBatch-KMeans to handle large datasets.
151
+ * Options = auto (switches to KMeans if ≥ 10,000 molecules), false = use HCA (for smaller datasets), true = use KMeans (for large datasets).
152
+ * Default = auto.
139
153
 
140
154
  #### Command line help
141
155
  A full description of the arguments can also be viewed directly from the command line using the command:
@@ -164,3 +178,4 @@ Veríssimo, G. C.; Panteleão, S. Q.; Fernandes, P. O.; Gertrudes, J. C.; Kronen
164
178
 
165
179
  ## References
166
180
  [1]: DE VOS, N. J. kmodes categorical clustering library. https://github.com/nicodv/kmodes. 2015-2021.
181
+
@@ -0,0 +1,19 @@
1
+ MASSA_Algorithm/MASSA.py,sha256=2CxEnm4DokqHVQqJvLFd0Z33EsOGA5lS7kERwCYAkj0,13333
2
+ MASSA_Algorithm/MASSAargs.py,sha256=rMlV-8ty-Cup7zufVaDY1QQv41azusgH_AwLJgxtEwc,12088
3
+ MASSA_Algorithm/MASSAcluster.py,sha256=aHYRHeuHxzGyiW3KIT4K3cCBddgnjKZj1U63cNCTv6U,21316
4
+ MASSA_Algorithm/MASSAdescriptors.py,sha256=g34_1CCJLwacj9qEfEyGe4Ci-5EKZc-DAHiFuEE8IC4,3066
5
+ MASSA_Algorithm/MASSAextraction.py,sha256=S5v9MqwLmVrw6Pke7d_gvaKgIvt8tT3U5w-Y_6q6lKw,7084
6
+ MASSA_Algorithm/MASSAlogos.py,sha256=c9EOJTAKTohUdSnYl30u2gogofUKtaTp02ZZls_5KaA,3629
7
+ MASSA_Algorithm/MASSAmoloutput.py,sha256=X-gMvIQ7oEkpdXdPEI5FevncDgnM02X4C-oQrOoaejI,3643
8
+ MASSA_Algorithm/MASSAod.py,sha256=C2pkFMo5icH9tKZxxsEuqjFMl8FyP7HlSaDqqKHYWHw,657
9
+ MASSA_Algorithm/MASSAopen_files.py,sha256=DDPA-zKmxxsRPkaAz8pKG_j3LK3acyXQAGdL2Mxhc5Q,10708
10
+ MASSA_Algorithm/MASSApreparation.py,sha256=PxKyZxvfZKt5lwrB2RtohwqWFLMzDPpscINZObYWqws,3476
11
+ MASSA_Algorithm/MASSAsplit.py,sha256=Gdch0jZY_bLmKWFWNeZ-b-Zmt_HkrdpN1ej2fU_u5gQ,16460
12
+ MASSA_Algorithm/__init__.py,sha256=yY3Tbs-t4HSl4I0Ev0RcHMnHU7SwljlWRveiRJkeCB8,10
13
+ MASSA_Algorithm/pyMASSA.py,sha256=Ow1ykPhFhYzttwuunbyf7Sxfayn0aUr6vwdE2hTAcNU,14154
14
+ massa_algorithm-2.0.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
15
+ massa_algorithm-2.0.1.dist-info/METADATA,sha256=CH99grh3pyZJUQGkqcwTTKlc7dQLH7GIMXiEKJkGMHw,10496
16
+ massa_algorithm-2.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ massa_algorithm-2.0.1.dist-info/entry_points.txt,sha256=oMtqswy07Di-w2uIYAEvenBKl0ovsr9AYm_yxn6LGtU,63
18
+ massa_algorithm-2.0.1.dist-info/top_level.txt,sha256=IZyttJ20RSD8w9nAcaTHWAvkvxHV5FGaVGB7mDrw9Cc,16
19
+ massa_algorithm-2.0.1.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- MASSA_Algorithm/MASSA.py,sha256=AKFJWuV43hrdFgNPSQjcg4J7NxEnu0Q0l67bxs_qDWo,11847
2
- MASSA_Algorithm/MASSAargs.py,sha256=5aq7Ne0v1xZmH59Pb5n_YiyErIT6Y0rd7fS6MnY_2jk,11163
3
- MASSA_Algorithm/MASSAcluster.py,sha256=yfZw1QE21NbDPv5pEeSzgCPMpOTPDQ2ZajvKrsBWDfA,13754
4
- MASSA_Algorithm/MASSAdescriptors.py,sha256=g34_1CCJLwacj9qEfEyGe4Ci-5EKZc-DAHiFuEE8IC4,3066
5
- MASSA_Algorithm/MASSAextraction.py,sha256=Xosjqwccbr1nXrgBXtRlUZrDFc05lFTtDUHWggbAC1k,5864
6
- MASSA_Algorithm/MASSAlogos.py,sha256=tlM8S91KBJNqZZHjVJ2UmQFARclIw7VgZR7c-YdAYPw,3532
7
- MASSA_Algorithm/MASSAmoloutput.py,sha256=41CvWOseca-LCUYn-tCITMY_RAmWd1f7z_9IX8c1jJo,3010
8
- MASSA_Algorithm/MASSAod.py,sha256=By50oZurFH7HJGtyRuvAhgxKNPATI15pRLVm0MYh9ww,595
9
- MASSA_Algorithm/MASSAopen_files.py,sha256=-naQ_m-p3ofZY2nZRNR2KsPxgS7vRqltma1Jp6W8raM,10729
10
- MASSA_Algorithm/MASSApreparation.py,sha256=Bck0TV-l_x2POGekqeXTRkQ4GvIXZ53iResBvSjbMrY,3543
11
- MASSA_Algorithm/MASSAsplit.py,sha256=_9i6g3y1NUurJQi9gKszowVeTFRYbhVntUu4EFkchsA,16363
12
- MASSA_Algorithm/__init__.py,sha256=yY3Tbs-t4HSl4I0Ev0RcHMnHU7SwljlWRveiRJkeCB8,10
13
- massa_algorithm-1.0.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
14
- massa_algorithm-1.0.0.dist-info/METADATA,sha256=gY3NKENHoticjapmsz-Tb0vCwfWxR6nEysxAshzrGoY,9023
15
- massa_algorithm-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
- massa_algorithm-1.0.0.dist-info/entry_points.txt,sha256=oMtqswy07Di-w2uIYAEvenBKl0ovsr9AYm_yxn6LGtU,63
17
- massa_algorithm-1.0.0.dist-info/top_level.txt,sha256=IZyttJ20RSD8w9nAcaTHWAvkvxHV5FGaVGB7mDrw9Cc,16
18
- massa_algorithm-1.0.0.dist-info/RECORD,,