Source code for NNSOM.plots

# Dynamically choose a parent class for your subclass based on whether CuPy is available
try:
    import cupy as cp
    from .som_gpu import SOMGpu
    base_class = SOMGpu
except ImportError:
    from .som import SOM
    base_class = SOM

from .utils import *

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib.widgets import Button
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
import matplotlib.colors as mcolors


[docs] class SOMPlots(base_class): """ A subclass of either SOM or SOMGpu (based on the availability of CuPy), designed to provide visualization and interactive plotting capabilities for Self-Organizing Maps (SOMs). This class is intended to enrich the analysis of SOMs by offering a variety of advanced visualization techniques to explore the trained SOM topology, distribution of data points, and various statistics derived from the SOM's learning process. Attributes: dimensions: A Gird of the SOM topology. Methods: The class includes methods for plotting the topology of the SOM, generating hit histograms, displaying cluster information, and more. These methods support interactive features through mouse clicks, allowing users to engage with the visualizations dynamically. Each method can also handle additional parameters for customization and handles various plotting styles like hexagonal units, numbered neurons, color gradients, and complex cluster histograms. The class also provides a generic plot method to handle different types of SOM visualizations and an event handling method to manage user interactions during the plotting sessions. This class supports interactivity and offers multiple visualization methods to deeply understand and analyze the behaviors and results of SOMs. It's especially useful for gaining insights into the topology, data distribution, and classification performance of SOMs. """
[docs] def __init__(self, dimensions): """ Initializes the SOMPlots class with specified dimensions for the SOM grid. This constructor sets up the underlying SOM or SOMGpu infrastructure, depending on the availability of CuPy. Parameters ---------- dimensions : array-like, tuple, or int A tuple specifying the dimensions (rows, columns) of the SOM grid. """ super().__init__(dimensions)
[docs] def plt_top(self, mouse_click=False, connect_pick_event=True, **kwargs): """ Plots the topology of the SOM using hexagonal units. This method visualizes the position and the boundaries of each neuron within the grid, allowing for interaction if enabled. Parameters ---------- mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron (hexagon) is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler `onpick` when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. patches : list A list of matplotlib.patches.Patch objects representing the hexagonal units of the SOM. """ w = self.w pos = self.pos numNeurons = self.numNeurons shapex, shapey = get_hexagon_shape() fig, ax = plt.subplots(frameon=False) plt.axis('equal') xmin = np.min(pos[0]) + np.min(shapex) xmax = np.max(pos[0]) + np.max(shapex) ymin = np.min(pos[1]) + np.min(shapey) ymax = np.max(pos[1]) + np.max(shapey) ax.set_xlim([xmin, xmax]) ax.set_ylim([ymin, ymax]) patches = [] for i in range(numNeurons): temp, = ax.fill(pos[0, i] + shapex, pos[1, i] + shapey, facecolor=(1, 1, 1), edgecolor=(0.8, 0.8, 0.8), picker=True) patches.append(temp) # Assign the cluster number for each hexagon hexagon_to_neuron = {hex: neuron for neuron, hex in enumerate(patches)} # Mouse Click Functionality if mouse_click and connect_pick_event: fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, patches, hexagon_to_neuron, **kwargs) ) # Get rid of extra white space on sides plt.tight_layout() return fig, ax, patches
[docs] def plt_top_num(self, mouse_click=False, connect_pick_event=True, **kwargs): """ Plots the topology of the SOM with each neuron numbered. This method visualizes each neuron as a hexagon with a number indicating its index, which is useful for identifying and referencing specific neurons during analysis. Parameters ---------- mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interaction such as detailed queries or data manipulation associated with specific neurons, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler `onpick` when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. patches : list A list of matplotlib.patches.Patch objects representing the hexagonal units of the SOM. text : list A list of matplotlib.text.Text objects displaying the neuron indices. """ w = self.w pos = self.pos numNeurons = self.numNeurons shapex, shapey = get_hexagon_shape() fig, ax = plt.subplots(frameon=False) ax.axis('off') plt.axis('equal') xmin = np.min(pos[0]) + np.min(shapex) xmax = np.max(pos[0]) + np.max(shapex) ymin = np.min(pos[1]) + np.min(shapey) ymax = np.max(pos[1]) + np.max(shapey) ax.set_xlim([xmin, xmax]) ax.set_ylim([ymin, ymax]) patches = [] for i in range(numNeurons): temp, = ax.fill(pos[0, i] + shapex, pos[1, i] + shapey, facecolor=(1, 1, 1), edgecolor=(0.8, 0.8, 0.8), picker=True) patches.append(temp) # Assign the cluster number for each hexagon hexagon_to_neuron = {hex: neuron for neuron, hex in enumerate(patches)} text = [] for i in range(numNeurons): temp = plt.text(pos[0, i], pos[1, i], str(i), horizontalalignment='center', verticalalignment='center', color='b') temp._fontproperties._weight = 'bold' temp._fontproperties._size = 12.0 text.append(temp) # Mouse Click Functionality if mouse_click and connect_pick_event: fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, patches, hexagon_to_neuron, **kwargs) ) # Get rid of extra white space on sides plt.tight_layout() return fig, ax, patches, text
[docs] def hit_hist(self, x, textFlag=True, mouse_click=False, connect_pick_event=True, **kwargs): """ Generates a hit histogram for the SOM, which displays the frequency of data points assigned to each neuron. Each neuron is represented as a hexagon, and the size of each hexagon is proportional to the number of hits. Optionally, the actual number of hits can be displayed within each hexagon. Parameters ---------- x : array-like The input data to be visualized in the histogram. textFlag : bool, optional If True, displays the count of hits within each hexagon, by default True. mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler `onpick` when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. patches : list A list of matplotlib.patches.Patch objects representing the inner hexagons colored based on hit counts. text : list, optional A list of matplotlib.text.Text objects displaying the hit counts, included if textFlag is True. """ pos = self.pos numNeurons = self.numNeurons # Determine the shape of the hexagon to represent each cluster shapex, shapey = get_hexagon_shape() # Create the main figure and axes # Set the main axes properties fig, ax = plt.subplots(figsize=(8, 8)) ax.set_aspect('equal') ax.set_axis_off() xmin = np.min(pos[0]) + np.min(shapex) xmax = np.max(pos[0]) + np.max(shapex) ymin = np.min(pos[1]) + np.min(shapey) ymax = np.max(pos[1]) + np.max(shapey) ax.set_xlim([xmin, xmax]) ax.set_ylim([ymin, ymax]) # Create the cluster hexagons hexagons = [] for i in range(numNeurons): hex, = ax.fill(pos[0, i] + shapex, pos[1, i] + shapey, facecolor=(1, 1, 1), edgecolor=(0.8, 0.8, 0.8), picker=True) hexagons.append(hex) # Assign cluster number for each hexagon hexagon_to_neuron = {hex: neuron for neuron, hex in enumerate(hexagons)} # Plot the inner hexagon patches = [] for i in range(numNeurons): temp = plt.fill(pos[0, i] + shapex, pos[1, i] + shapey, edgecolor=None) patches.append(temp) # Add the size of the cluster, if needed text = [] if textFlag: for i in range(numNeurons): temp = plt.text(pos[0, i], pos[1, i], '9', horizontalalignment='center', verticalalignment='center', color='w') temp._fontproperties._weight = 'bold' temp._fontproperties._size = 12.0 text.append(temp) # Compute the SOM outputs for the data set if self.sim_flag: x = self.normalize(x, self.norm_func) outputs = self.sim_som(x) outputs = outputs self.sim_flag = False else: outputs = self.outputs # Find out how many inputs fall into each cluster hits = np.sum(outputs, axis=1) norm_hits = np.sqrt(hits/np.amax(hits)) shapex1 = np.append(shapex, shapex[0]) shapey1 = np.append(shapey, shapey[0]) # Make the size of the inner hexagon proportional to the cluster size for i in range(numNeurons): patches[i][0]._facecolor = (0.4, 0.4, 0.6, 1.0) patches[i][0]._edgecolor = (0.2, 0.2, 0.3, 1.0) patches[i][0]._path._vertices[:, 0] = pos[0, i] + shapex1 * norm_hits[i] patches[i][0]._path._vertices[:, 1] = pos[1, i] + shapey1 * norm_hits[i] if textFlag: text[i]._text = str(int(hits[i])) # Get rid of extra white space on sides # plt.axis('off') #fig.tight_layout() # Mouse Click Functionality if mouse_click and connect_pick_event: fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, hexagons, hexagon_to_neuron, **kwargs) ) return fig, ax, patches, text
[docs] def gray_hist(self, x, perc, mouse_click=False, **kwargs): """ Generates a grayscale histogram for the SOM, where the shade of each hexagon represents the corresponding value from the provided percentage array. Parameters ---------- x : array-like The input data to be visualized in the histogram. perc : array-like An array containing the percentage values to be represented by the grayscale shades, where higher values correspond to darker shades. mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. patches : list A list of matplotlib.patches.Patch objects representing the inner hexagons colored based on the grayscale values. text : list, optional A list of matplotlib.text.Text objects displaying the hit counts, included if textFlag is True in the underlying `hit_hist` method. """ numNeurons = self.numNeurons dmax = np.amax(np.abs(perc)) # Find the maximum value of perc across all clusters fig, ax, patches, text = self.hit_hist(x, False, mouse_click, **kwargs) # Scale the gray scale to the perc value for neuron in range(numNeurons): scale = perc[neuron] / dmax color = [scale for i in range(3)] # Create a gray color based on the scaled value color.append(1.0) # Add alpha value patches[neuron][0]._facecolor = tuple(color) # Apply the color to the patch # Get rid of extra white space on sides plt.tight_layout() return fig, ax, patches, text
[docs] def color_hist(self, x, avg, mouse_click=False, **kwargs): """ Generates a colored histogram for the SOM, where the color of each hexagon represents the corresponding value from the provided average array. Parameters ---------- x : array-like The input data to be visualized in the histogram. avg : array-like An array containing the average values to be represented by the color map, where higher values correspond to warmer colors (e.g., red) and lower values correspond to cooler colors (e.g., blue). mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. patches : list A list of matplotlib.patches.Patch objects representing the inner hexagons colored based on the average values. text : list, optional A list of matplotlib.text.Text objects displaying the hit counts, included if textFlag is True in the underlying `hit_hist` method. cbar : matplotlib.colorbar.Colorbar The Colorbar object attached to the plot, representing the color mapping. """ # Find the maximum value of avg across all clusters dmax = np.amax(np.abs(avg)) numNeurons = self.numNeurons fig, ax, patches, text = self.hit_hist(x, False, mouse_click, **kwargs) # Use the jet color map cmap = plt.get_cmap('jet') xx = np.zeros(numNeurons) # Adjust the color of the hexagon according to the avg value for neuron in range(numNeurons): xx[neuron] = avg[neuron] / dmax color = cmap(xx[neuron]) patches[neuron][0]._facecolor = color plt.tight_layout() # # Add a color bar the the figure to indicate levels # # create an axes on the right side of ax. The width of cax will be 5% # # of ax and the padding between cax and ax will be fixed at 0.05 inch. # divider = make_axes_locatable(ax) # cax = divider.append_axes("right", size="5%", pad=0.05) # # cbar = plt.colorbar(ax, cax=cax, cmap=cmap) cax = cm.ScalarMappable(cmap=cmap) cax.set_array(xx) cbar = fig.colorbar(cax, ax=ax) # Adjust the tick labels to the correct scale ticklab = cbar.ax.get_yticks() numticks = len(ticklab) ticktext = [] for i in range(numticks): ticktext.append('%.2f' % (dmax * ticklab[i])) # Set the ticks first cbar.ax.set_yticks(ticklab) cbar.ax.set_yticklabels(ticktext) # Get rid of extra white space on sides fig.tight_layout() return fig, ax, patches, text, cbar
[docs] def cmplx_hit_hist(self, x, clust, perc, ind_missClass, ind21, ind12, mouse_click=False, **kwargs): """ Generates a complex hit histogram for the SOM, incorporating information about cluster quality, misclassifications, and false positives/negatives. Parameters ---------- x : array-like The input data to be visualized in the histogram. clust : list or array-like A list or array containing the cluster assignments for each data point. perc : array-like An array containing the percentage values for each cluster, representing the proportion of good binders. ind_missClass : array-like An array containing the indices of misclassified data points. ind21 : array-like An array containing the indices of false positive data points (classified as good binders but are actually bad binders). ind12 : array-like An array containing the indices of false negative data points (classified as bad binders but are actually good binders). mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. patches : list A list of matplotlib.patches.Patch objects representing the inner hexagons colored and styled based on cluster quality and misclassifications. text : list A list of matplotlib.text.Text objects displaying the hit counts within each hexagon. """ numNeurons = self.numNeurons if mouse_click: kwargs['clust'] = clust # Make hit histogram fig, ax, patches, text = self.hit_hist(x, True, mouse_click, **kwargs) for neuron in range(numNeurons): # Make face color green if majority of class in cluster are good binders if (perc[neuron] >= 50): patches[neuron][0]._facecolor = (0.0, 1.0, 0.0, 1.0) if len(np.intersect1d(clust[neuron], ind_missClass)) != 0: # If there are errors in the cluster, change width of # hexagon edge in proportion to number of errors lwidth = 20. * len(np.intersect1d(clust[neuron], ind_missClass)) / len(clust[neuron]) if len(np.intersect1d(clust[neuron], ind12)) > len(np.intersect1d(clust[neuron], ind21)): # Make edge color red if most errors are false positive color = (1.0, 0.0, 0.0, 1.0) else: # Make edge color purple if most errors are false negative color = (1.0, 0.0, 1.0, 1.0) else: lwidth = 0 color = (1.0, 1.0, 1.0, 0.0) patches[neuron][0]._linewidth = lwidth patches[neuron][0]._edgecolor = color # Get rid of extra white space on sides plt.tight_layout() return fig, ax, patches, text
[docs] def custom_cmplx_hit_hist(self, x, face_labels, edge_labels, edge_width, mouse_click=False, **kwargs): """ Generates a custom complex hit histogram for the SOM, allowing for flexible customization of the hexagon face colors, edge colors, and edge widths. Parameters ---------- x : array-like The input data to be visualized in the histogram. face_labels : array-like An array containing the labels or values to be represented by the face colors of the hexagons. edge_labels : array-like An array containing the labels or values to be represented by the edge colors of the hexagons. edge_width : array-like An array containing the values for the edge widths of the hexagons. mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. patches : list A list of matplotlib.patches.Patch objects representing the inner hexagons colored and styled based on the provided face labels, edge labels, and edge widths. text : list A list of matplotlib.text.Text objects displaying the hit counts within each hexagon. Raises ------ ValueError If the input data or label arrays have incorrect dimensions or lengths. """ numNeurons = self.numNeurons x = np.asarray(x, np.float32) face_labels = np.asarray(face_labels, np.float32) edge_labels = np.asarray(edge_labels, np.float32) edge_width = np.asarray(edge_width, np.float32) # Check if the input data is a sequence of vectors if x.ndim != 2: raise ValueError("x must be a 2D array") # Check if the input data can be cdist with self.w # the input data must be transposed if x.shape[1] != self.w.shape[1]: raise ValueError("The input data must have the same number of features as the SOM") # Check if the face color, line width and edge color are 1D arrays if face_labels.ndim != 1 or edge_width.ndim != 1 or edge_labels.ndim != 1: raise ValueError("fcolor, lwidth and ecolor must be 1D arrays") # Check if the length of fcolor, lwidth and ecolor are equal to the number of neurons if len(face_labels) != numNeurons or len(edge_width) != numNeurons or len(edge_labels) != numNeurons: raise ValueError("The length of x, fcolor, lwidth and ecolor must be equal to the number of neurons") # Make hit histogram fig, ax, patches, text = self.hit_hist(x, True, mouse_click, **kwargs) # Exclude nan values for the unique color count unique_fcolor = np.unique(face_labels[~np.isnan(face_labels)]) unique_ecolor = np.unique(edge_labels[~np.isnan(edge_labels)]) # Create the colormaps cmap1 = plt.get_cmap('jet', len(unique_fcolor)) cmap2 = plt.get_cmap('cool', len(unique_ecolor)) for neuron in range(numNeurons): if not np.isnan(face_labels[neuron]): # Normalize the class label to the colormap index color1_idx = np.argwhere(unique_fcolor == face_labels[neuron])[0][0] / (len(unique_fcolor) - 1) color2_idx = np.argwhere(unique_ecolor == edge_labels[neuron])[0][0] / (len(unique_ecolor) - 1) # Get the corresponding color from the colormap patches[neuron][0]._facecolor = cmap1(color1_idx) patches[neuron][0]._linewidth = edge_width[neuron] patches[neuron][0]._edgecolor = cmap2(color2_idx) # Get rid of extra white space on sides plt.tight_layout() return fig, ax, patches, text
[docs] def plt_nc(self, mouse_click=False, connect_pick_event=True, **kwargs): """ Generates a Neighborhood Connection Map for the SOM, displaying the connections between neighboring neurons. Parameters ---------- mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. patches : list A list of matplotlib.patches.Patch objects representing the edges between connected neurons. """ # Neighborhood Connection Map. The gray hexagons represent cluster centers. pos = self.pos numNeurons = self.numNeurons # Determine the hexagon shape shapex, shapey = get_hexagon_shape() shapex, shapey = shapex * 0.3, shapey * 0.3 # Determine the elongated hexagon shape edgex, edgey = get_edge_shape() # Set up edges neighbors = np.zeros((numNeurons, numNeurons)) neighbors[self.neuron_dist <= 1.001] = 1.0 neighbors = np.tril(neighbors - np.identity(numNeurons)) # Get the figure and axes fig, ax = plt.subplots(figsize=(8, 8), frameon=False) ax.axis('equal') ax.axis('off') xmin = np.min(pos[0]) + np.min(shapex) xmax = np.max(pos[0]) + np.max(shapex) ymin = np.min(pos[1]) + np.min(shapey) ymax = np.max(pos[1]) + np.max(shapey) ax.set_xlim([xmin, xmax]) ax.set_ylim([ymin, ymax]) # Draw elongated hexagons between neurons patches = [] for i in range(numNeurons): for j in np.where(neighbors[:, i] == 1.0)[0]: pdiff = pos[:, j] - pos[:, i] angle = np.arctan2(pdiff[1], pdiff[0]) ex, ey = rotate_xy(edgex, edgey, angle) edgePos = (pos[:, i] + pos[:, j]) * 0.5 p1 = (2 * pos[:, i] + 1 * pos[:, j]) / 3 p2 = (1 * pos[:, i] + 2 * pos[:, j]) / 3 temp = ax.fill(edgePos[0] + ex, edgePos[1] + ey, facecolor='none', edgecolor=(0.8, 0.8, 0.8)) patches.append(temp) ax.plot([p1[0], p2[0]], [p1[1], p2[1]], '-', color=[1, 0, 0]) # Setup neurons. Place gray hexagon at neuron locations. hexagons = [] for i in range(numNeurons): hex, = ax.fill(pos[0, i] + shapex, pos[1, i] + shapey, facecolor=(0.4, 0.4, 0.6), edgecolor=(0.8, 0.8, 0.8), picker=True) hexagons.append(hex) # Assign the cluster number for each hexagon hexagon_to_neuron = {hex: neuron for neuron, hex in enumerate(hexagons)} if mouse_click and connect_pick_event: fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, hexagons, hexagon_to_neuron, **kwargs) ) return fig, ax, patches
[docs] def neuron_dist_plot(self, mouse_click=False, connect_pick_event=True, **kwargs): """ Generates a neuron distance plot that visualizes the distances between neighboring neurons in the SOM grid. Parameters ---------- mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. patches : list A list of matplotlib.patches.Patch objects representing the edges between connected neurons, colored based on the distance between their respective neuron weights. """ pos = self.pos numNeurons = self.numNeurons # Determine the shape of the hexagon to represent each cluster symmetry = 6 z = np.sqrt(0.75)/3 shapex = np.array([-1, 0, 1, 1, 0, -1]) * 0.5 shapey = np.array([1, 2, 1, -1, -2, -1]) * z edgex = np.array([-1, 0, 1, 0]) * 0.5 edgey = np.array([0, 1, 0, - 1]) * z shapex = shapex * 0.3 shapey = shapey * 0.3 # Set up edges neighbors = np.zeros((numNeurons, numNeurons)) neighbors[self.neuron_dist <= 1.001] = 1.0 neighbors = np.tril(neighbors - np.identity(numNeurons)) # Get the figure, remove the frame, and find the limits # of the axis that will fit hexagons fig, ax = plt.subplots(figsize=(8, 8)) ax.set_aspect('equal') ax.set_axis_off() xmin = np.min(pos[0]) + np.min(shapex) xmax = np.max(pos[0]) + np.max(shapex) ymin = np.min(pos[1]) + np.min(shapey) ymax = np.max(pos[1]) + np.max(shapey) ax.set_xlim([xmin, xmax]) ax.set_ylim([ymin, ymax]) # Draw elongated hexagons between neurons numEdges = int(np.sum(neighbors)) patches = [] for i in range(numNeurons): for j in np.where(neighbors[:, i] == 1.0)[0]: pdiff = pos[:, j]-pos[:, i] angle = np.arctan2(pdiff[1], pdiff[0]) ex, ey = rotate_xy(edgex, edgey, angle) edgePos = (pos[:, i] + pos[:, j]) * 0.5 p1 = (2 * pos[:, i] + 1 * pos[:, j]) / 3 p2 = (1 * pos[:, i] + 2 * pos[:, j]) / 3 temp = plt.fill(edgePos[0]+ex, edgePos[1]+ey, facecolor=np.random.rand(1,3), edgecolor='none') patches.append(temp) plt.plot([p1[0], p2[0]], [p1[1], p2[1]], '-', color=[1, 0, 0]) # Setup neurons. Place gray hexagon at neuron locations. hexagons = [] for i in range(numNeurons): hex, = ax.fill(pos[0, i] + shapex, pos[1, i] + shapey, facecolor=(0.4, 0.4, 0.6), edgecolor=(0.8, 0.8, 0.8), picker=True) hexagons.append(hex) # Assign the cluster number for each hexagon hexagon_to_neuron = {hex: neuron for neuron, hex in enumerate(hexagons)} # Find the distance between neighboring weights. weights = self.w levels = np.zeros(numEdges) k = 0 for i in range(numNeurons): for j in np.where(neighbors[:, i] == 1.0)[0]: levels[k] = np.sqrt(np.sum((weights[i, :] - weights[j, :]) ** 2)) k = k + 1 mn = np.amin(levels) mx = np.amax(levels) if mx == mn: levels = np.zeros(1, numEdges) + 0.5 else: levels = (levels - mn)/(mx - mn) # Make the face color black for the maximum distance and # yellow for the minimum distance. The middle distance will # be red. k = 0 for i in range(numNeurons): for j in np.where(neighbors[:, i] == 1.0)[0]: level = 1 - levels[k] red = np.amin([level * 2, 1]) green = np.amax([level * 2 - 1, 0]) c = (red, green, 0, 1.0) patches[k][0]._facecolor = c k = k + 1 # Mouse Click Functionality if mouse_click and connect_pick_event: fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, hexagons, hexagon_to_neuron, **kwargs) ) return fig, ax, patches
[docs] def simple_grid(self, avg, sizes, mouse_click=False, connect_pick_event=True, **kwargs): """ Generates a simple grid plot that visualizes the SOM neurons as hexagons with varying sizes and colors. Parameters ---------- avg : array-like An array containing the average values to be represented by the color map, where higher values correspond to warmer colors (e.g., red) and lower values correspond to cooler colors (e.g., blue). sizes : array-like An array containing the sizes to be used for the inner hexagons within each neuron, where larger values result in larger hexagons. mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. patches : list A list of matplotlib.patches.Patch objects representing the inner hexagons colored based on the average values and sized based on the provided sizes. cbar : matplotlib.colorbar.Colorbar The Colorbar object attached to the plot, representing the color mapping. """ w = self.w pos = self.pos numNeurons = self.numNeurons # Determine the shape of the hexagon to represent each cluster z = np.sqrt(0.75) shapex = np.array([-1, 0, 1, 1, 0, -1]) * 0.5 shapey = np.array([1, 2, 1, -1, -2, -1]) * (z / 3) # Get the figure, remove the frame, and find the limits # of the axis that will fit all of the hexagons fig, ax = plt.subplots(figsize=(8, 8)) plt.axis('equal') plt.axis('off') xmin = np.min(pos[0]) + np.min(shapex) xmax = np.max(pos[0]) + np.max(shapex) ymin = np.min(pos[1]) + np.min(shapey) ymax = np.max(pos[1]) + np.max(shapey) ax.set_xlim([xmin, xmax]) ax.set_ylim([ymin, ymax]) # Draw the outer hexagons hexagons = [] for i in range(numNeurons): hex, = ax.fill(pos[0, i] + shapex, pos[1, i] + shapey, facecolor=(0.4, 0.4, 0.6), edgecolor=(0.8, 0.8, 0.8), picker=True) hexagons.append(hex) # Assign the cluster number for each hexagon hexagon_to_neuron = {hex: neuron for neuron, hex in enumerate(hexagons)} # Plot the inner hexagon patches = [] for i in range(numNeurons): temp = plt.fill(pos[0, i] + shapex, pos[1, i] + shapey, edgecolor=None) patches.append(temp) shapex1 = np.append(shapex, shapex[0]) shapey1 = np.append(shapey, shapey[0]) # Make the size of the inner hexagon proportional to the desired size sizes = np.sqrt(sizes / np.amax(sizes)) for i in range(numNeurons): patches[i][0]._facecolor = (0.4, 0.4, 0.6, 1.0) patches[i][0]._edgecolor = (0.2, 0.2, 0.3, 1.0) patches[i][0]._path._vertices[:, 0] = pos[0, i] + shapex1 * sizes[i] patches[i][0]._path._vertices[:, 1] = pos[1, i] + shapey1 * sizes[i] # Find the maximum value of avg across all clusters # dmax = np.amax(np.abs(avg)) dmax = np.amax(avg) dmin = np.amin(avg) drange = dmax - dmin # Use the jet color map cmap = plt.get_cmap('jet') xx = np.zeros(numNeurons) # Adjust the color of the hexagon according to the avg value for neuron in range(numNeurons): # xx[neuron] = avg[neuron] / dmax xx[neuron] = (avg[neuron] - dmin) / drange color = cmap(xx[neuron]) patches[neuron][0]._facecolor = color plt.tight_layout() # # Add a color bar the the figure to indicate levels # # create an axes on the right side of ax. The width of cax will be 5% # # of ax and the padding between cax and ax will be fixed at 0.05 inch. cax = cm.ScalarMappable(cmap=cmap) cax.set_array(xx) # cbar = fig.colorbar(cax) cbar = fig.colorbar(cax, ax = ax, fraction=0.046, pad=0.04) # plt.colorbar(im, fraction=0.046, pad=0.04) # # divider = make_axes_locatable(ax) # cax = divider.append_axes("right", size="5%", pad=0.05) # # plt.colorbar(im, cax=cax) # Adjust the tick labels to the correct scale ticklab = cbar.ax.get_yticks() numticks = len(ticklab) ticktext = [] for i in range(numticks): ticktext.append('%.2f' % (drange * ticklab[i] + dmin)) cbar.ax.set_yticks(ticklab) cbar.ax.set_yticklabels(ticktext) if mouse_click and connect_pick_event: fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, hexagons, hexagon_to_neuron, **kwargs) ) plt.tight_layout() return fig, ax, patches, cbar
def setup_axes(self): # Setup figure, axes and sub-axes for plots pos = self.pos numNeurons = self.numNeurons # Determine the hexagon shape shapex, shapey = get_hexagon_shape() shminx = np.min(shapex) shmaxx = np.max(shapex) shminy = np.min(shapey) shmaxy = np.max(shapey) # Create the figure and get the transformations from data # to pixel and from pixel to axes. fig, ax = plt.subplots(frameon=False, figsize=(8, 8), layout='constrained') # Set the main axes properties xmin = np.min(pos[0]) + np.min(shapex) xmax = np.max(pos[0]) + np.max(shapex) ymin = np.min(pos[1]) + np.min(shapey) ymax = np.max(pos[1]) + np.max(shapey) ax.set_xlim([xmin, xmax]) ax.set_ylim([ymin, ymax + 0.5]) ax.axis('off') ax.set_aspect('equal') # Draw hexagon hexagons = [] for neuron in range(numNeurons): hex, = ax.fill(pos[0, neuron] + shapex, pos[1, neuron] + shapey, facecolor=(1, 1, 1), edgecolor=(0.8, 0.8, 0.8), picker=True) hexagons.append(hex) # Assign the cluster number for each hexagon hexagon_to_neuron = {hex: neuron for neuron, hex in enumerate(hexagons)} # Loop over to create sub-axe in each cluster h_axes = [0] * numNeurons # A container for sub-axes for neuron in range(numNeurons): # Find the size of the cell in data units minx = pos[0, neuron] + shminx maxx = pos[0, neuron] + shmaxx miny = pos[1, neuron] + shminy maxy = pos[1, neuron] + shmaxy # Convert the size of the cell to axes units minxyDis = ax.transData.transform([minx, miny]) maxxyDis = ax.transData.transform([maxx, maxy]) minxyAx = ax.transAxes.inverted().transform(minxyDis) maxxyAx = ax.transAxes.inverted().transform(maxxyDis) # Find the width and height of the cell width = maxxyAx[0] - minxyAx[0] height = maxxyAx[1] - minxyAx[1] # Find the center point of the cell xavg = np.average([minxyAx[0], maxxyAx[0]]) yavg = np.average([minxyAx[1], maxxyAx[1]]) # Scale the width and height scale = np.sqrt(0.75) / 3 * 2 # Just fit-in the hexagon width = width * scale height = height * scale # Locate the beginning point of the cell x0 = xavg - (width / 2) y0 = yavg - (height / 2) # Create sub-axes h_axes[neuron] = inset_axes(ax, width='100%', height='100%', loc=3, bbox_to_anchor=(x0, y0, width, height), bbox_transform=ax.transAxes, borderpad=0) h_axes[neuron].set(xticks=[], yticks=[]) h_axes[neuron].set_frame_on(False) return fig, ax, h_axes, hexagons, hexagon_to_neuron
[docs] def plt_stem(self, x, y, mouse_click=False, connect_pick_event=True, **kwargs): """ Generates a stem plot visualization for the SOM, displaying the input data and neuron responses. Parameters ---------- x : array-like The input data or independent variable for the stem plot. y : array-like The neuron responses or dependent variable for the stem plot, where each row corresponds to a neuron. mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. h_axes : list A list of matplotlib.axes.Axes objects, each containing a stem plot for a single neuron. """ numNeurons = self.numNeurons # Setup figure, axes, and sub-axes fig, ax, h_axes, hexagons, hexagon_to_neuron = self.setup_axes() # Draw stem plot for neuron in range(numNeurons): # Make graph h_axes[neuron].stem(x, y[neuron]) if mouse_click and connect_pick_event: kwargs['cat'] = y fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, hexagons, hexagon_to_neuron, **kwargs) ) return fig, ax, h_axes
[docs] def plt_wgts(self, mouse_click=False, connect_pick_event=True, **kwargs): """ Generates a line plot visualization for the SOM weights, displaying the weight vectors for each neuron. Parameters ---------- mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. h_axes : list A list of matplotlib.axes.Axes objects, each containing a line plot for a single neuron's weight vector. """ numNeurons = self.numNeurons w = self.w # Setup figure, main axes, and sub-axes fig, ax, h_axes, hexagons, hexagon_to_neuron = self.setup_axes() # Draw line plots for neuron in range(numNeurons): # Make graph h_axes[neuron].plot(w[neuron]) if mouse_click and connect_pick_event: fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, hexagons, hexagon_to_neuron, **kwargs) ) return fig, ax, h_axes
[docs] def plt_pie(self, x, s=None, mouse_click=False, connect_pick_event=True, **kwargs): """ Generates a pie chart visualization for the SOM, displaying the composition of each neuron's data or cluster. Parameters ---------- x : array-like A 2D array or sequence of vectors, where each row represents the composition or category values for a single neuron. s : array-like, optional An array containing the percentage values to be used for scaling the pie chart sizes, by default None. mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. h_axes : list A list of matplotlib.axes.Axes objects, each containing a pie chart for a single neuron's composition. Raises ------ ValueError If the length of `x` or `s` (if provided) does not match the number of neurons, or if the percentage values in `s` are not between 0 and 100. """ # Validate the length of x (array or sequence of vectors) if len(x) != self.numNeurons: raise ValueError("The length of x must be equal to the number of neurons.") # Validate perc values if s is not None: s = np.array(s) if np.any(s < 0) or np.any(s > 100): raise ValueError("Percentage values must be between 0 and 100.") # Validate the length of perc if s is not None and len(s) != self.numNeurons: raise ValueError("The length of s must be equal to the number of neurons.") numNeurons = self.numNeurons # Determine the number of colors needed shapclust = x.shape num_colors = shapclust[1] # Generate a color list using a colormap cmap = cm.get_cmap('plasma', num_colors) # Use any suitable clrs = [cmap(i) for i in range(num_colors)] # Setup figure, main axes, and sub-axes fig, ax, h_axes, hexagons, hexagon_to_neuron = self.setup_axes() # Draw pie plot in each neuron for neuron in range(numNeurons): # Determine the scale of the pie chart if s is None: scale = 1 else: scale = np.sqrt(s[neuron] / 100) scale = max(scale, 0.01) # Ensure minimum scale # Make pie chart if np.sum(x[neuron]) != 0: h_axes[neuron].pie(x[neuron], colors=clrs, radius=scale) else: h_axes[neuron] = None if mouse_click and connect_pick_event: kwargs['cat'] = x fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, hexagons, hexagon_to_neuron, **kwargs) ) return fig, ax, h_axes
[docs] def plt_histogram(self, x, mouse_click=False, connect_pick_event=True, **kwargs): """ Generates a histogram visualization for the SOM, displaying the data distribution within each neuron's cluster. Parameters ---------- x : array-like A 2D array or sequence of vectors, where each row represents the data points assigned to a single neuron. mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. h_axes : list A list of matplotlib.axes.Axes objects, each containing a histogram for a single neuron's data distribution. """ numNeurons = self.numNeurons # Setup figure, main axes, and sub-axes fig, ax, h_axes, hexagons, hexagon_to_neuron = self.setup_axes() # Draw histogram for neuron in range(numNeurons): if len(x[neuron]) > 0: # Make graph h_axes[neuron].hist(x[neuron]) # Enable the axes for this histogram h_axes[neuron].set_frame_on(True) h_axes[neuron].tick_params(axis='both', which='both', length=5) # Show tick marks h_axes[neuron].set(xticks=[], yticks=[]) # Show only the left and bottom spines h_axes[neuron].spines['top'].set_visible(False) h_axes[neuron].spines['right'].set_visible(False) h_axes[neuron].spines['left'].set_visible(True) h_axes[neuron].spines['bottom'].set_visible(True) else: h_axes[neuron] = None if mouse_click and connect_pick_event: kwargs['num1'] = x fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, hexagons, hexagon_to_neuron, **kwargs) ) return fig, ax, h_axes
[docs] def plt_boxplot(self, x, mouse_click=False, connect_pick_event=True, **kwargs): """ Generates a boxplot visualization for the SOM, displaying the statistical summary of the data distribution within each neuron's cluster. Parameters ---------- x : array-like A 2D array or sequence of vectors, where each row represents the data points assigned to a single neuron. mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. h_axes : list A list of matplotlib.axes.Axes objects, each containing a boxplot for a single neuron's data distribution. """ numNeurons = self.numNeurons # Setup figure, main axes, and sub-axes fig, ax, h_axes, hexagons, hexagon_to_neuron = self.setup_axes() # Find global min and max across all neuron's data global_min, global_max = get_global_min_max(x) for neuron in range(numNeurons): if len(x[neuron]) > 0: # Make graph h_axes[neuron].boxplot(x[neuron]) # Set the same y axis limits for all subplots h_axes[neuron].set_ylim(global_min, global_max) # h_axes[neuron].set_yticks(np.linspace(global_min, global_max, 5)) else: h_axes[neuron] = None if mouse_click and connect_pick_event: kwargs['num1'] = x fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, hexagons, hexagon_to_neuron, **kwargs) ) return fig, ax, h_axes
[docs] def plt_violin_plot(self, x, mouse_click=False, connect_pick_event=True, **kwargs): """ Generates a violin plot visualization for the SOM, displaying the distribution of data within each neuron's cluster. Parameters ---------- x : array-like A 2D array or sequence of vectors, where each row represents the data points assigned to a single neuron. mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. h_axes : list A list of matplotlib.axes.Axes objects, each containing a violin plot for a single neuron's data distribution. """ numNeurons = self.numNeurons # Setup figure, main axes, and sub-axes fig, ax, h_axes, hexagons, hexagon_to_neuron = self.setup_axes() # Find global min and max across all neuron's data global_min, global_max = get_global_min_max(x) for neuron in range(numNeurons): if len(x[neuron]) > 0: # Make graph on the appropriate sub-axes h_axes[neuron].violinplot(x[neuron]) # Set the same y axis limits for all subplots h_axes[neuron].set_ylim(global_min, global_max) # h_axes[neuron].set_yticks(np.linspace(global_min, global_max, 5)) else: h_axes[neuron] = None if mouse_click and connect_pick_event: kwargs['num1'] = x fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, hexagons, hexagon_to_neuron, **kwargs) ) return fig, ax, h_axes
[docs] def plt_scatter(self, x, y, reg_line=True, mouse_click=False, connect_pick_event=True, **kwargs): """ Generates a scatter plot visualization for the SOM, displaying the data points assigned to each neuron and an optional regression line. Parameters ---------- x : array-like A 2D array or sequence of vectors, where each row represents the x-coordinate data points assigned to a single neuron. y : array-like A 2D array or sequence of vectors, where each row represents the y-coordinate data points assigned to a single neuron. reg_line : bool, optional If True, a regression line is plotted for each neuron's data, by default True. mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. h_axes : list A list of matplotlib.axes.Axes objects, each containing a scatter plot for a single neuron's data. """ pos = self.pos numNeurons = self.numNeurons # Setup figure, main axes, and sub-axes fig, ax, h_axes, hexagons, hexagon_to_neuron = self.setup_axes() # Determine the global minimum and maximum of x and y for the axes limits x_min, x_max = get_global_min_max(x) y_min, y_max = get_global_min_max(y) # Loop over each neuron for hexagons and scatter plots for neuron in range(numNeurons): # Make Scatter Plot for each neuron if len(x[neuron]) > 0 and len(y[neuron]) > 0: h_axes[neuron].scatter(x[neuron], y[neuron], s=1, c='k') if reg_line: m, p = np.polyfit(x[neuron], y[neuron], 1) h_axes[neuron].plot(x[neuron], m * x[neuron] + p, c='r', linewidth=1) # Set the same x and y limits for each sub-plot based on global min and max h_axes[neuron].set_xlim(x_min, x_max) h_axes[neuron].set_ylim(y_min, y_max) # Show only the left and bottom spines h_axes[neuron].spines['top'].set_visible(False) h_axes[neuron].spines['right'].set_visible(False) h_axes[neuron].spines['left'].set_visible(True) h_axes[neuron].spines['bottom'].set_visible(True) # Enable the axes and show tick marks h_axes[neuron].set_frame_on(True) h_axes[neuron].tick_params(axis='both', which='both', length=5) else: h_axes[neuron] = None if mouse_click and connect_pick_event: kwargs['num1'] = x kwargs['num2'] = y fig.canvas.mpl_connect( 'pick_event', lambda event: self.onpick(event, hexagons, hexagon_to_neuron, **kwargs) ) return fig, ax, h_axes
[docs] def component_positions(self, x): """ Visualizes the positions of the components in a Self-Organizing Map (SOM) along with the input vectors. This method plots the trained SOM weight vectors as gray dots and the input vectors as green dots on a 2D plot. It also connects neighboring SOM neurons with red lines to represent the grid structure, illustrating the organization and clustering within the map. Parameters ---------- x : array-like A 2D array or sequence of vectors, typically representing input data or test data that has been projected onto the SOM. """ x = np.transpose(x) # Extract the trained weight vectors and the SOM grid dimensions weight_vectors = self.w grid_x, grid_y = self.dimensions # Plot the SOM weight vectors as gray dots plt.scatter(weight_vectors[:, 0], weight_vectors[:, 1], color='gray', s=50, label='Weight Vectors') # for i, vec in enumerate(weight_vectors): # plt.annotate(str(i), (vec[0], vec[1]), textcoords="offset points", xytext=(0,5), ha='center') # Plot the Iris data points as blue dots plt.scatter(x[:, 0], x[:, 1], color='green', s=20, label='Input Vectors', alpha=0.5) # Draw red lines to connect neighboring neurons for i in range(grid_x): for j in range(grid_y): index = i * grid_y + j # Calculate the linear index of the neuron in the SOM neuron = weight_vectors[index] # Connect to the right neighbor if it exists if j < grid_y - 1: right_index = i * grid_y + (j + 1) right_neighbor = weight_vectors[right_index] plt.plot([neuron[0], right_neighbor[0]], [neuron[1], right_neighbor[1]], color='red') # Connect to the bottom neighbor if it exists if i < grid_x - 1: bottom_index = (i + 1) * grid_y + j bottom_neighbor = weight_vectors[bottom_index] plt.plot([neuron[0], bottom_neighbor[0]], [neuron[1], bottom_neighbor[1]], color='red') # Set labels and legend plt.xlabel('Weight 1') plt.ylabel('Weight 2') plt.title('SOM Weight Positions') plt.legend() plt.grid(False) plt.show()
[docs] def component_planes(self, X): """ Visualizes the weight distribution across different features in a Self-Organizing Map (SOM) using a series of 2D plots. This method creates a grid of subplots where each subplot represents the weight distribution for a specific feature of the input data. The weight of each neuron for the given feature is represented in the plot by the color of a hexagonal cell, with darker colors indicating higher weights. This visualization helps in understanding the importance and distribution of each feature across the map. Parameters ---------- X : array-like A 2D array of input data where each row represents a feature and each column represents a sample. """ w = self.w pos = self.pos numNeurons = self.numNeurons z = np.sqrt(0.75) shapex = np.array([-1, 0, 1, 1, 0, -1]) * 0.5 shapey = np.array([1, 2, 1, -1, -2, -1]) * (z / 3) num_features = X.shape[0] grid_size = int(np.ceil(np.sqrt(num_features))) # Calculate grid size fig, axes = plt.subplots(grid_size, grid_size, figsize=(10, 10)) for i, ax in enumerate(axes.flatten()): if i < num_features: ax.axis('equal') xmin = np.min(pos[0]) + np.min(shapex) xmax = np.max(pos[0]) + np.max(shapex) ymin = np.min(pos[1]) + np.min(shapey) ymax = np.max(pos[1]) + np.max(shapey) ax.set_xlim([xmin, xmax]) ax.set_ylim([ymin, ymax]) # Get the weights for the current feature feature_weights = w[:, i] # Normalize the weights to range between 0 and 1 norm = mcolors.Normalize(vmin=np.min(feature_weights), vmax=np.max(feature_weights)) for j in range(numNeurons): color = plt.cm.viridis(norm(feature_weights[j])) # Choose colormap as viridis inverted_color = tuple( 1 - np.array(color[:3])) # Invert the color to make darker colors represent larger weights ax.fill(pos[0, j] + shapex, pos[1, j] + shapey, facecolor=inverted_color, edgecolor=(0.8, 0.8, 0.8)) plt.show()
[docs] def weight_as_image(self, rows=None, mouse_click=False, connect_pick_event=True, **kwargs): """ Visualizes the weights of a Self-Organizing Map (SOM) as images within a hexagonal grid layout. This method maps the weight vectors of each neuron onto a hexagonal cell and optionally enables interaction with each hexagon. The hexagons represent the neurons, and the colors within each hexagon represent the neuron's weight vector reshaped into either a specified or automatically determined matrix form. This visualization is useful for analyzing the learned patterns and feature representations within the SOM. Parameters ---------- rows : int, optional The number of rows to reshape each neuron's weight vector into. If None, the weight vector is reshaped into a square matrix by default. If specified, the weight vector is reshaped into a matrix with the given number of rows, and the number of columns is determined automatically. mouse_click : bool, optional If True, enables the plot to respond to mouse clicks, allowing for interactive functionality such as querying or modifying neuron data, by default False. connect_pick_event : bool, optional If True, connects a pick event that triggers when a neuron is clicked, by default True. **kwargs : dict Arbitrary keyword arguments that can be passed to the event handler onpick when an interactive element is clicked. Common parameters could include data specific to the plot or visualization settings. Returns ------- fig : matplotlib.figure.Figure The Figure object containing the plot. ax : matplotlib.axes.Axes The Axes object containing the plot elements. patches : list A list of matplotlib.patches.Patch objects, each representing a hexagon in the plot. """ w = self.w # Weight matrix pos = self.pos # Positions of the neurons numNeurons = self.numNeurons # Number of neurons # Get the shape of a single hexagon shapex, shapey = get_hexagon_shape() # Create the figure and axis with a larger size to accommodate the decorations fig, ax = plt.subplots(figsize=(8, 6)) # Set the aspect of the plot to be equal plt.axis('equal') # List to keep track of the hexagon patches patches = [] for i in range(numNeurons): hex_center_x = pos[0, i] # x coordinate of the ith position hex_center_y = pos[1, i] # y coordinate of the ith position # Draw the hexagon temp, = ax.fill(hex_center_x + shapex, hex_center_y + shapey, facecolor='none', edgecolor='k', picker=True) patches.append(temp) # Assign the cluster number for each hexagon hexagon_to_neuron = {hex: neuron for neuron, hex in enumerate(patches)} # Transform the row of weights into a matrix if necessary if rows is None: weight_matrix = w[i].reshape(int(np.sqrt(w.shape[1])), -1) # Default to square matrix for simplicity else: weight_matrix = w[i].reshape(rows, -1) # Calculate the size and position for the imshow plot # Find the radius of the hexagon, accounting for the scaling of the shape hex_radius = (np.max(shapex) - np.min(shapex)) / 2 # Calculate the side length of the hexagon for a regular hexagon side_length = hex_radius * np.sqrt(3) / 2 # Offset the inset_axes to be centered within the hexagon # The factor of sqrt(3)/2 is because in a regular hexagon, the distance from the center to a side is sqrt(3)/2 times the side length axins = ax.inset_axes([hex_center_x - side_length / 2, hex_center_y - side_length * (np.sqrt(3) / 2) / 2, side_length, side_length * (np.sqrt(3) / 2)], transform=ax.transData) # Ensure the imshow plot takes up the correct amount of space # Adjust aspect ratio if necessary, depending on the weight matrix shape aspect_ratio = weight_matrix.shape[0] / weight_matrix.shape[1] axins.imshow(weight_matrix, aspect='equal') axins.set_aspect(aspect_ratio * (side_length / (side_length * (np.sqrt(3) / 2)))) # Turn off the axis axins.axis('off') # Connect the pick event for interactivity if required if mouse_click and connect_pick_event: fig.canvas.mpl_connect('pick_event', lambda event: self.onpick(event, patches, hexagon_to_neuron, **kwargs)) # Adjust the layout to fit everything plt.tight_layout() # Display the plot plt.show() # Return the figure components return fig, ax, patches
# Generic Plot Function
[docs] def plot(self, plot_type, data_dict=None, ind=None, target_class=None, use_add_array=False, **kwargs): """ Generic Plot Function. It generates a plot based on the plot type and data provides. Parameters ---------- plot_type : str The type of plot to be generated: ["top", "top_num", "hit_hist", "gray_hist", "color_hist", "complex_hist", "nc", "neuron_dist", "simple_grid", "stem", "pie", "wgts", "pie", "hist", "box", "violin", "scatter", "component_positions", "component_planes"] data_dict: dict (optional) A dictionary containing the data to be plotted. The key is prefixed with the data type and the value is the data itself. {"data", "target", "clust", "add_1d_array", "add_2d_array"} ind : int, str or array-like (optional) The indices of the data to be plotted. target_class: int (optional) The target class to be plotted. use_add_array: bool (optional) If true, the additional array to be used. **kwargs : dict Additional arguments to be passed to the interactive plot function. """ # Plot Types allowed plot_types = ["top", "top_num", "hit_hist", "gray_hist", "color_hist", "complex_hist", "neuron_connection", "neuron_dist", "simple_grid", "stem", "pie", "wgts", "hist", "box", "violin", "scatter", "component_positions", "component_planes"] # Validate the plot type if plot_type not in plot_types: raise ValueError(f"Invalid plot type: {plot_type}") # Validate the data_dict if data_dict is None and plot_type not in ["top", "top_num", "neuron_connection", "neuron_dist", "wgts"]: raise ValueError("data_dict is required for this plot type.") # Validate the plot function plot_functions = { "top": self.plt_top, "top_num": self.plt_top_num, "hit_hist": self.hit_hist, "gray_hist": self.gray_hist, "color_hist": self.color_hist, "complex_hist": self.custom_cmplx_hit_hist, "neuron_connection": self.plt_nc, "neuron_dist": self.neuron_dist_plot, "simple_grid": self.simple_grid, "stem": self.plt_stem, "pie": self.plt_pie, "wgts": self.plt_wgts, "hist": self.plt_histogram, "box": self.plt_boxplot, "violin": self.plt_violin_plot, "scatter": self.plt_scatter, "component_positions": self.component_positions, "component_planes": self.component_planes } # Assign the plot function selected_plot = plot_functions.get(plot_type) # Error Handling if the plot function recieve the appropriate arguments def validate_data_dict(keys): for key in keys: if key not in data_dict: raise ValueError(f"{key} is required for this plot type.") # ======== Topology, Neuron Connection, Neuron Distance, and Weight Plot ========== if plot_type in ["top", "top_num", "neuron_connection", "neuron_dist", "wgts"]: # Call the plot function return selected_plot(**kwargs) # ======== Components Plane Family ========== elif plot_type in ['component_positions', 'component_planes']: # Error Handling if the data_dict have the scaled input data X validate_data_dict(["data"]) # Data Preparation x = data_dict['data'] x = self.normalize(x, self.norm_func) # Invoke Function return selected_plot(x) # ===================== Hit Histogram Family ===================== elif plot_type in ['hit_hist', 'gray_hist', 'color_hist', 'complex_hist']: # Validate the data_dict have the scaled input data validate_data_dict(["data"]) # Extract input data x = data_dict['data'] if plot_type in ['hit_hist']: # Invoke the function return selected_plot(x, True, **kwargs) elif plot_type in ['gray_hist']: # Gray Hist with add_1d_array if use_add_array: # Validate the data_dict have the additional 1D array validate_data_dict(["add_1d_array"]) if len(data_dict['add_1d_array']) != self.numNeurons: raise ValueError( "The additional 1D array must have the same length as the clust data or original data.") perc = data_dict['add_1d_array'] # Gray hist with data else: # Error Handling if the target data not provided validate_data_dict(["clust"]) if target_class is None and ind is None: raise ValueError("This plot requires either the target class or ind.") elif target_class is not None and ind is not None: raise ValueError("This plot requires only either the target class or ind.") elif target_class is not None: validate_data_dict(['target']) perc = get_perc_cluster(data_dict['target'], target_class, data_dict['clust']) elif ind is not None: feature = x[:, ind] perc = get_cluster_avg(feature, data_dict['clust']) # Invoke the gray hist function return selected_plot(x, perc, **kwargs) elif plot_type in ['color_hist']: # Color Hist with additional data if use_add_array: validate_data_dict(['add_1d_array']) if len(data_dict['add_1d_array']) != self.numNeurons: raise ValueError("The additional 1D array must have the same length as the clust data or original data.") avg = data_dict['add_1d_array'] # Color Hist with input data else: validate_data_dict(["clust"]) if target_class is None and ind is None: raise ValueError("This plot requires either the target class or ind.") elif target_class is not None and ind is not None: raise ValueError("This plot requires only either the target class or ind.") elif target_class is not None: validate_data_dict(['target']) avg = get_perc_cluster(data_dict['target'], target_class, data_dict['clust']) elif ind is not None: feature = x[:, ind] avg = get_cluster_avg(feature, data_dict['clust']) return selected_plot(x, avg, **kwargs) elif plot_type in ['complex_hist']: if use_add_array: # Validate the data_dict have the additional 2D array validate_data_dict(["add_2d_array"]) add_2d_array = np.array(data_dict['add_2d_array']) if add_2d_array.shape[0] != self.numNeurons: raise ValueError( "The additional 2D array must have the same length as the number of neurons.") # Assuming add_2d_array is a list of lists if add_2d_array.shape[1] != 3: raise ValueError("Each inner list in the additional 2D array must have exactly 3 items. \ E.g. [numNeurons, [face_labels, edge_labels, edge_widths[0-20]]") # Extract Data face_labels = add_2d_array[:, 0] edge_labels = add_2d_array[:, 1] edge_widths = add_2d_array[:, 2] else: raise ValueError("This plot requires an additional 2-D array in data_dict. " "The additional 2D array must have 3 features. " "E.g. [numNeurons, [face_labels, edge_labels, edge_widths[0-20]]") return selected_plot(x, face_labels, edge_labels, edge_widths, **kwargs) # ===================== Simple Grid ===================== elif plot_type in ['simple_grid']: # Validate the data_dict have the original data validate_data_dict(["data"]) data = data_dict['data'] # Simple grid with addtional variable if use_add_array: # Error Handlig if the additional 2D arrays not privided validate_data_dict(["add_2d_array"]) add_2d_array = np.asarray(data_dict["add_2d_array"], np.float32) # Validate Length if add_2d_array.shape[0] != self.numNeurons: raise ValueError("The additional 2D array must have the same length as the number of cluster") # Validate number of items in each cluster if add_2d_array.shape[1] != 2: raise ValueError("Each cluster must have only 2 items in the additional 2D array") avg = add_2d_array[:, 0] sizes = add_2d_array[:, 1] else: # Error Handling if the target and clust data not provided validate_data_dict(["clust", "target"]) if ind is None: raise ValueError("The indices is required for this plot type.") if target_class is None: raise ValueError("The target class is required") clust = data_dict['clust'] # Extract avg from the original data num_feature = data[:, ind] avg = get_cluster_avg(num_feature, clust) # Extract size from the target target = data_dict['target'] sizes = get_perc_cluster(target, target_class, clust) return selected_plot(avg, sizes, **kwargs) # ===================== Basic Plot Family ===================== elif plot_type in ['stem', 'pie']: # If the user want to plot addtional data if use_add_array: # Error Handling if the additional 2D array not provided validate_data_dict(["add_2d_array"]) # Error Handling if additional vategorical variable has correct length if len(data_dict["add_2d_array"]) != self.numNeurons: raise ValueError("The additional categorical data must have the same length as the clust data.") # Get Additional Data sizes = data_dict['add_2d_array'] else: # Error Handling if the clust data not provided validate_data_dict(["target", "clust"]) # Extract Information clust = data_dict['clust'] target = data_dict['target'] sizes = count_classes_in_cluster(target, clust) if plot_type == 'pie': # ============================================= # It needs to handle scale (need to implement) # ============================================= # Call the pie plot return selected_plot(sizes, **kwargs) elif plot_type == 'stem': # Extract Align if use_add_array: align = [i for i in range(sizes.shape[1])] else: align = [i for i in range(len(np.unique(target)))] # Call the stem plot return selected_plot(align, sizes, **kwargs) elif plot_type in ['hist']: # Error Handling if the index not provided if ind is None: raise ValueError("The indices is required for this plot type.") # Error Handling if the original data not provided validate_data_dict(["data", "clust"]) # Extract the feature from the original data clust = data_dict['clust'] feature = data_dict['data'][:, ind] x = get_cluster_array(feature, clust) return selected_plot(x, **kwargs) elif plot_type in ['scatter']: # Error Handling if the index not provided if ind is None: raise ValueError("The indices is required for this plot type.") if len(ind) != 2: raise ValueError("The indices must contain exactly two elements. Eg. [0, 1]") # Error Handling if the original data and clust not provided validate_data_dict(["data", "clust"]) # Extract the feature from the original data x = data_dict['data'][:, ind[0]] y = data_dict['data'][:, ind[1]] clust = data_dict['clust'] x = get_cluster_array(x, clust) y = get_cluster_array(y, clust) # Call the scatter plot function return selected_plot(x, y, **kwargs) elif plot_type in ['box', 'violin']: # Error Handling if the original data not provided validate_data_dict(["data", "clust"]) # Extract the feature from the original data clust = data_dict['clust'] if ind is None: # Extract All data data = data_dict['data'] x = get_cluster_data(data, clust) elif isinstance(ind, int): # index just have 1 index data = data_dict['data'][:, ind] x = get_cluster_array(data, clust) elif isinstance(ind, (list, np.ndarray)): # index have multiple indices data = data_dict['data'][:, ind] x = get_cluster_data(data, clust) # Call the box plot and violin function return selected_plot(x, **kwargs)
# Interactive Functionality def onpick(self, event, hexagons, hexagon_to_neuron, **kwargs): """ Interactive Plot Function Parameters ---------- event: event a mouse click event hexagons: list a list of hexagons hexagon_to_neuron: dict a dictionary mapping hexagons to neurons **kwargs: a dictionary with input data Returns: None """ if event.artist not in hexagons: return # Detect the clicked hexagon thishex = event.artist neuron_ind = hexagon_to_neuron[thishex] if len(kwargs["clust"][neuron_ind]) <= 0: print('No data in this cluster') return # Create a new window fig, ax = plt.subplots(figsize=(6, 6)) fig.subplots_adjust(right=0.8) ax.set_aspect('equal') # Button Configuration button_types = self.determine_button_types(**kwargs) buttons = create_buttons(fig, button_types) # Store buttons in an attribute to maintain a reference self.buttons = buttons # Set up button click events for button_type, button in self.buttons.items(): button.on_clicked(self.create_click_handler(button_type, ax, neuron_ind, **kwargs)) # Show up the 2nd window plt.show() def create_click_handler(self, button_type, ax, neuron_ind, **kwargs): # Generates a custom event handler for button clicks in a plot. def handler(event): self.button_click_event(button_type, ax, neuron_ind, **kwargs) return handler def button_click_event(self, button_type, ax, neuron_ind, **kwargs): # Handle button click event by calling the appropriate plot function if button_type == 'pie': # Pre-process categorical variables sizes = kwargs['cat'] sizes = sizes[neuron_ind][:kwargs['topn']] self.plot_pie(ax, sizes, neuron_ind) elif button_type == 'stem': sizes = kwargs['cat'] # Generate the align array: 0, 1, 2, ..., number of unique item -1 align = np.arange(len(sizes[0])) self.plot_stem(ax, align, sizes, neuron_ind) elif button_type == 'hist': num1 = kwargs['num1'][neuron_ind][:kwargs['topn']] self.plot_hist(ax, num1, neuron_ind) elif button_type == 'box': # Pre-process continuous variables nums = [] for key in kwargs: if key.startswith('num'): nums.append(kwargs[key][neuron_ind][:kwargs['topn']]) self.plot_box(ax, nums, neuron_ind) elif button_type == 'violin': # Pre-process continuous variables nums = [] for key in kwargs: if key.startswith('num'): nums.append(kwargs[key][neuron_ind][:kwargs['topn']]) self.plot_violin(ax, nums, neuron_ind) elif button_type == 'scatter': # Pre-process continuous variables nums = [] for key in kwargs: if key.startswith('num'): nums.append(kwargs[key][neuron_ind][:kwargs['topn']]) self.plot_scatter(ax, nums[0], nums[1], neuron_ind) elif button_type == 'sub_cluster': cluster_data = get_cluster_data(kwargs['data'], kwargs['clust']) sub_clust_data = cluster_data[neuron_ind] # Get the data for the self.sub_clustering(sub_clust_data, neuron_ind) else: print(f"Unknown button type: {button_type}") def determine_button_types(self, **kwargs): # Determine the button type based on the contents of **kwargs button_types = [] # Check for categorical data for pie charts if 'cat' in kwargs and kwargs['cat'] is not None: button_types.append('pie') button_types.append('stem') # Check for numerical data and decide which buttons to add num_keys = [key for key in kwargs if key.startswith('num') and isinstance(kwargs[key], (list, np.ndarray)) and len(kwargs[key]) > 0] if num_keys: button_types.extend(['hist', 'box', 'violin']) # Add 'scatter' button only if there are at least two numerical columns if len(num_keys) >= 2: button_types.append('scatter') # Assuming sub-clustering is always an option if 'data' in kwargs: button_types.append('sub_cluster') return button_types # Helper function to create charts def plot_pie(self, ax, data, neuronNum): """ Plots a pie chart on the specified matplotlib axes object. Parameters ---------- ax : matplotlib.axes.Axes The matplotlib axes object where the pie chart will be plotted. data : array-like An array of numeric data which represents the portions of the pie chart. neuronNum : int The neuron number associated with the data, which is used to title the pie chart. """ # Clear the axes ax.clear() # Pie chart plot logic here # Determine the number of colors needed num_colors = len(data) cmap = cm.get_cmap('plasma', num_colors) clrs = [cmap(i) for i in range(num_colors)] ax.pie(data, colors=clrs, autopct='%1.1f%%') ax.set_title('Pie Chart inside the Cluster ' + str(neuronNum)) # Redraw the figure ax.figure.canvas.draw_idle() def plot_stem(self, ax, align, height, neuronNum): """ Plots a stem plot for a specific neuron's data on the given axes Parameters ---------- ax : matplotlib.axes.Axes The matplotlib axes object where the stem plot will be drawn. align : array-like The x positions of the stems. height : array-like The y values for each stem, indexed by neuron number. neuronNum : int The index of the neuron for which the plot is being generated. """ # Clear the axes ax.clear() # Stem plot ax.stem(align, height[neuronNum]) # x: cat, y: data ax.set_title('Stem Plot inside the Clluster ' + str(neuronNum)) # Redraw the figure ax.figure.canvas.draw_idle() def plot_hist(self, ax, data, neuronNum): """ Helper function to plot histogram in the interactive plots Args: ax: data: neuronNum: Returns: """ # Clear the axes ax.clear() # Histogram plot logic here ax.hist(data) ax.set_title('Histogram inside the Cluster ' + str(neuronNum)) # Redraw the figure ax.figure.canvas.draw_idle() def plot_box(self, ax, data, neuronNum): """ Generates a box plot for a specific neuron's data on the provided axes. Parameters ---------- ax : matplotlib.axes.Axes The axes object on which the box plot will be drawn. data : array-like The data array for which the box plot is to be generated. neuronNum : int The neuron index that the data is associated with. """ # Clear the axes ax.clear() # Box plot logic here ax.boxplot(data) ax.set_title("Box plot in the Cluster " + str(neuronNum)) # Redraw the figure ax.figure.canvas.draw_idle() def plot_violin(self, ax, data, neuronNum): """ Displays a violin plot for a specific neuron's data on the provided axes. Parameters ---------- ax : matplotlib.axes.Axes The axes object where the violin plot will be drawn. data : array-like The data to be used for the violin plot. neuronNum : int The index of the neuron associated with the data. """ # Clear the axes ax.clear() # Violin plot logic here ax.violinplot(data) ax.set_title('Violin Plot inside the Cluster ' + str(neuronNum)) # Redraw the figure ax.figure.canvas.draw_idle() def plot_scatter(self, ax, num1, num2, neuronNum): """ Plots a scatter plot for a specific neuron's data on the provided axes. Parameters ---------- ax : matplotlib.axes.Axes The axes object on which the scatter plot will be drawn. num1 : array-like The x coordinates of the data points. num2 : array-like The y coordinates of the data points. neuronNum : int The index of the neuron for which the plot is being generated. """ # Clear the axes ax.clear() # Scatter plot logic here ax.scatter(num1, num2) ax.set_title('Scatter Plot inside the Cluster ' + str(neuronNum)) # Redraw the figure ax.figure.canvas.draw_idle() def sub_clustering(self, data, neuron_ind): """ Performs sub-clustering on the data associated with a specific neuron within the Self-Organizing Map (SOM). Parameters ---------- data : array-like The dataset from which sub-clusters are to be derived. Typically, this is the subset of the overall dataset that has been mapped to the neuron specified by `neuron_ind`. neuron_ind : int The index of the neuron for which sub-clustering is to be performed. This index is used to refer to a specific neuron in the SOM's grid. Returns ------- list of array-like A list of clusters, where each cluster is an array of data points that form a sub-group within the neuron's data. """ if len(data) <= 1: print("There is no enough data to create sub-cluster") return if neuron_ind in self.sub_som: print('Sub clustering already done') sub_clust = self.sub_som[neuron_ind] else: # Training Sub Cluster sub_clust = SOMPlots((2, 2)) sub_clust.init_w(data, norm_func=self.norm_func) sub_clust.train(data, 3, 500, 100, norm_func=self.norm_func) self.sub_som[neuron_ind] = sub_clust # Plot the sub cluster <- Can we h fig, ax, patches, text = sub_clust.hit_hist(data, True, connect_pick_event=False) plt.show()