From 8f9803f5dc60129177db7a8163135007ad699c1e Mon Sep 17 00:00:00 2001 From: salva24 Date: Sun, 26 Apr 2026 13:53:54 +0200 Subject: [PATCH 1/2] minor fixes: avoid very unlikely division by 0 in data generator, change graph names, use set in thresholds for efficiency, refine example demo1, fix bug with double legend in plot discontinuous --- examples/demo1.py | 2 +- src/gaufs/data_generator.py | 3 +++ src/gaufs/gaufs.py | 24 +++++++++++------------- src/gaufs/genetic_search.py | 3 +-- src/gaufs/utils.py | 7 ++++--- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/examples/demo1.py b/examples/demo1.py index b641ad3..14f9889 100644 --- a/examples/demo1.py +++ b/examples/demo1.py @@ -38,7 +38,7 @@ def main(): os.chdir(script_dir) # Filepath - file_path = os.path.join("datasets", "corners_3clusters.csv") + file_path = os.path.join("datasets", "corners_5clusters.csv") # Generate data with corners distribution and save it to a CSV file data_with_labels = DataGenerator.generate_data_corners( diff --git a/src/gaufs/data_generator.py b/src/gaufs/data_generator.py index 9cdb908..1bc4421 100644 --- a/src/gaufs/data_generator.py +++ b/src/gaufs/data_generator.py @@ -588,6 +588,9 @@ def _generate_cluster_centered( # significant dimensions # Generate random direction vector denom = 0.0 + #avoid division by zero in case the random vector is zero + denom = 1.0 if denom == 0.0 else denom + for j in range(num_dims_sig): vector_aux[j] = random.uniform(-1, 1) denom = denom + vector_aux[j] ** 2 diff --git a/src/gaufs/gaufs.py b/src/gaufs/gaufs.py index df0306d..b0078f1 100644 --- a/src/gaufs/gaufs.py +++ b/src/gaufs/gaufs.py @@ -834,7 +834,7 @@ def run(self): f"Optimal number of clusters: {self._optimal_variable_selection_and_num_of_clusters[1]}\n" ) f.write( - f"Fitness: {self._optimal_variable_selection_and_num_of_clusters}\n\n" + f"Fitness: {self._fitness_of_optimal_variable_selection_and_num_of_clusters}\n\n" ) f.write("Selected Variables:\n") f.write( @@ -888,7 +888,7 @@ def run(self): return ( self._optimal_variable_selection_and_num_of_clusters, - self._optimal_variable_selection_and_num_of_clusters, + self._fitness_of_optimal_variable_selection_and_num_of_clusters, ) def run_genetic_searches(self): @@ -1002,7 +1002,7 @@ def analyze_variable_weights(self): "Variable significances not computed. Please run genetic searches with `run_genetic_searches` before analyzing variable weights." ) - thresholds = sorted(self._variable_significance, reverse=True) + thresholds = sorted(set(self._variable_significance), reverse=True) # list of the considered selections possible_variable_selections = [] @@ -1228,7 +1228,7 @@ def plot_dictionaries(self): _plot_discontinuous(ax1, x1, y1, marker="o", color="black") ax1.set_xlabel("Number of Selected Variables") ax1.set_ylabel("Number of Clusters") - ax1.set_title("Number of Clusters for Each Selection") + ax1.set_title(r"Number of Clusters for Each Selection $\Psi_{\text{num\_clusters}}$") ax1.grid(True, alpha=0.3) ax1.xaxis.set_major_locator(plt.MaxNLocator(integer=True)) ax1.yaxis.set_major_locator(plt.MaxNLocator(integer=True)) @@ -1239,7 +1239,7 @@ def plot_dictionaries(self): ax2.plot(x2, y2, marker="o", color="tab:blue") ax2.set_xlabel("Number of Selected Variables") ax2.set_ylabel("Fitness") - ax2.set_title("Fitness for Each Selection") + ax2.set_title(r"Fitness for Each Selection $\tilde{\Psi}_{\text{fitness}}$") ax2.grid(True, alpha=0.3) ax2.xaxis.set_major_locator(plt.MaxNLocator(integer=True)) @@ -1249,7 +1249,7 @@ def plot_dictionaries(self): ax3.plot(x3, y3, marker="o", color="tab:blue") ax3.set_xlabel("Number of Selected Variables") ax3.set_ylabel("Threshold") - ax3.set_title("Threshold for Each Selection") + ax3.set_title(r"Threshold for Each Selection $\tilde{\Psi}_{\text{weight}}$") ax3.grid(True, alpha=0.3) ax3.xaxis.set_major_locator(plt.MaxNLocator(integer=True)) @@ -1257,7 +1257,7 @@ def plot_dictionaries(self): ax4 = axes[1, 1] x4, y4 = zip(*sorted(self._dict_num_var_selected_importance.items())) ax4.plot( - x4, y4, marker="o", label="Selected Variables' Importance", color="navy" + x4, y4, marker="o", label=r"Selected Variables' Importance $\Phi_{\text{average}}$", color="navy" ) x5, y5 = zip( @@ -1271,7 +1271,7 @@ def plot_dictionaries(self): marker="x", s=50, color="red", - label="Delta Importance with Exp Decay", + label=r"Delta Importance with Exp Decay $\tilde{\delta}_i$", ) x_argmax = sum(self._optimal_variable_selection_and_num_of_clusters[0]) @@ -1279,7 +1279,7 @@ def plot_dictionaries(self): x=x_argmax, color="black", linestyle="--", - label=f"Automatic solution with {x_argmax} variables achieving a fitness of: {self._dict_num_var_selected_fitness[x_argmax]:.3f}", + label=rf"Automatic solution: {x_argmax} = $\hat{{i}}$ variables and fitness of: {self._dict_num_var_selected_fitness[x_argmax]:.3f}" ) ax4.set_xlabel("Number of Selected Variables") @@ -1462,7 +1462,6 @@ def get_plot_comparing_solution_with_another_metric( label=f"Automatic solution with {x_argmax} variables achieving a fitness of: {self._dict_num_var_selected_fitness[x_argmax]:.3f}", ) ax1.xaxis.set_major_locator(plt.MaxNLocator(integer=True)) - ax1.legend() # Right plot: Variables vs External Metric # plot with discontinuities @@ -1487,7 +1486,6 @@ def get_plot_comparing_solution_with_another_metric( label=f"Automatic solution with {x_argmax} variables achieving a metric of: {external_metrics[num_vars.index(x_argmax)]:.3f}", ) ax2.xaxis.set_major_locator(plt.MaxNLocator(integer=True)) - ax2.legend() # Add baseline scores for true number of labels if true_number_of_labels is not None: @@ -1511,8 +1509,8 @@ def get_plot_comparing_solution_with_another_metric( color="red", label="Metric for each selection with True Number of Labels", ) - ax1.legend() - ax2.legend() + ax1.legend() + ax2.legend() plt.tight_layout() diff --git a/src/gaufs/genetic_search.py b/src/gaufs/genetic_search.py index 4124bdd..d74a3ec 100644 --- a/src/gaufs/genetic_search.py +++ b/src/gaufs/genetic_search.py @@ -91,7 +91,6 @@ def __init__( self.num_clusters_and_its_max_fitness = dict() # dictionary={variable_selection:(maximum fitness found for that selection (regardeless of the cluster number), number of times a chromosome with this selection entered in the hof)} self.hof_counter = dict() - self.hof_weighted = dict() # logbook to store statistics self.logbook = tools.Logbook() @@ -336,7 +335,7 @@ def run(self): self.hof_counter[variables_selection] = ( max(old[0], ind.fitness.values[0]), old[1] + 1, - ) # maximo fitness y cuento las veces que ha entrado + ) # max fitness and times it entered in the hof else: self.hof_counter[variables_selection] = (ind.fitness.values[0], 1) diff --git a/src/gaufs/utils.py b/src/gaufs/utils.py index 0106090..6c5ce03 100644 --- a/src/gaufs/utils.py +++ b/src/gaufs/utils.py @@ -246,6 +246,8 @@ def _clear_directory(directory_path): # Helper function to plot with discontinuities def _plot_discontinuous(ax, x_vals, y_vals, **kwargs): """Plot lines only between consecutive x values""" + # Extract label from kwargs + label = kwargs.pop("label", None) i = 0 while i < len(x_vals): # Find the end of the consecutive sequence @@ -253,13 +255,12 @@ def _plot_discontinuous(ax, x_vals, y_vals, **kwargs): while j < len(x_vals) - 1 and x_vals[j + 1] == x_vals[j] + 1: j += 1 - # Plot this consecutive segment - ax.plot(x_vals[i : j + 1], y_vals[i : j + 1], **kwargs) + # Plot this consecutive segment: only the first segment carries the label + ax.plot(x_vals[i : j + 1], y_vals[i : j + 1], label=label if i == 0 else None, **kwargs) # Move to next segment i = j + 1 - def _convert_to_serializable(obj): """ Function to convert numpy arrays to lists From 65e03f3057ecc2dd810108593f9c2f2d74fc629d Mon Sep 17 00:00:00 2001 From: salva24 Date: Sun, 26 Apr 2026 13:58:24 +0200 Subject: [PATCH 2/2] black formatting --- src/gaufs/data_generator.py | 2 +- src/gaufs/gaufs.py | 12 +++++++++--- src/gaufs/utils.py | 10 ++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/gaufs/data_generator.py b/src/gaufs/data_generator.py index 1bc4421..60ab894 100644 --- a/src/gaufs/data_generator.py +++ b/src/gaufs/data_generator.py @@ -588,7 +588,7 @@ def _generate_cluster_centered( # significant dimensions # Generate random direction vector denom = 0.0 - #avoid division by zero in case the random vector is zero + # avoid division by zero in case the random vector is zero denom = 1.0 if denom == 0.0 else denom for j in range(num_dims_sig): diff --git a/src/gaufs/gaufs.py b/src/gaufs/gaufs.py index b0078f1..b751a16 100644 --- a/src/gaufs/gaufs.py +++ b/src/gaufs/gaufs.py @@ -1228,7 +1228,9 @@ def plot_dictionaries(self): _plot_discontinuous(ax1, x1, y1, marker="o", color="black") ax1.set_xlabel("Number of Selected Variables") ax1.set_ylabel("Number of Clusters") - ax1.set_title(r"Number of Clusters for Each Selection $\Psi_{\text{num\_clusters}}$") + ax1.set_title( + r"Number of Clusters for Each Selection $\Psi_{\text{num\_clusters}}$" + ) ax1.grid(True, alpha=0.3) ax1.xaxis.set_major_locator(plt.MaxNLocator(integer=True)) ax1.yaxis.set_major_locator(plt.MaxNLocator(integer=True)) @@ -1257,7 +1259,11 @@ def plot_dictionaries(self): ax4 = axes[1, 1] x4, y4 = zip(*sorted(self._dict_num_var_selected_importance.items())) ax4.plot( - x4, y4, marker="o", label=r"Selected Variables' Importance $\Phi_{\text{average}}$", color="navy" + x4, + y4, + marker="o", + label=r"Selected Variables' Importance $\Phi_{\text{average}}$", + color="navy", ) x5, y5 = zip( @@ -1279,7 +1285,7 @@ def plot_dictionaries(self): x=x_argmax, color="black", linestyle="--", - label=rf"Automatic solution: {x_argmax} = $\hat{{i}}$ variables and fitness of: {self._dict_num_var_selected_fitness[x_argmax]:.3f}" + label=rf"Automatic solution: {x_argmax} = $\hat{{i}}$ variables and fitness of: {self._dict_num_var_selected_fitness[x_argmax]:.3f}", ) ax4.set_xlabel("Number of Selected Variables") diff --git a/src/gaufs/utils.py b/src/gaufs/utils.py index 6c5ce03..e42a70f 100644 --- a/src/gaufs/utils.py +++ b/src/gaufs/utils.py @@ -247,7 +247,7 @@ def _clear_directory(directory_path): def _plot_discontinuous(ax, x_vals, y_vals, **kwargs): """Plot lines only between consecutive x values""" # Extract label from kwargs - label = kwargs.pop("label", None) + label = kwargs.pop("label", None) i = 0 while i < len(x_vals): # Find the end of the consecutive sequence @@ -256,11 +256,17 @@ def _plot_discontinuous(ax, x_vals, y_vals, **kwargs): j += 1 # Plot this consecutive segment: only the first segment carries the label - ax.plot(x_vals[i : j + 1], y_vals[i : j + 1], label=label if i == 0 else None, **kwargs) + ax.plot( + x_vals[i : j + 1], + y_vals[i : j + 1], + label=label if i == 0 else None, + **kwargs, + ) # Move to next segment i = j + 1 + def _convert_to_serializable(obj): """ Function to convert numpy arrays to lists