Source code for popclass.visualization

"""
Light visualization library.
"""
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.axes_grid1 import make_axes_locatable

color_cycler = [
    "#009988",
    "#EE3377",
    "#EE7733",
    "#33BBEE",
    "#CC3311",
    "#0077BB",
]

cmap_cycler = ["Greens", "RdPu", "Oranges", "Blues", "Reds", "Purples"]

marker_cycler = ["o", "^", "*", "s", "H"]


[docs] def get_bounds(PopulationModel, parameters): """ Creates bounds on the basis of a PopulationModel and given parameters, if they are not specified by the user. Bounds are automatically constructed to be 10% of the extent in samples beyond the minimum and maximum value found in samples for each parameter. Args: PopulationModel (class) - as defined in model.py, class containing the population samples and parameters parameters (list of str) - a subset of parameters for visualization (must be found in PopulationModel.parameters) Returns ------- bounds (numpy.ndarray) - pairs of upper and lower bounds for each parameter. Shape (N_dim, 2), where N_dim is equal to the number of parameters and has the same order. """ ndim = len(parameters) classes = PopulationModel.classes bounds = np.array([[0.0, 0.0] for i in range(ndim)]) samples_all = np.concatenate( ( [ PopulationModel.samples(class_name=class_name, parameters=parameters) for class_name in classes ] ) ) for counter, param in enumerate(parameters): param_min, param_max = np.min(samples_all[:, counter]), np.max( samples_all[:, counter] ) param_lower, param_upper = ( param_min - (param_max - param_min) / 10, param_max + (param_max - param_min) / 10, ) bounds[counter] = param_lower, param_upper return bounds
[docs] def plot_population_model( PopulationModel, parameters=None, plot_samples=False, plot_kdes=True, bounds=None, N_bins=200, N_hist=40, levels=5, legend=False, ): """ Represent the population samples and/or their KDEs in the defined parameter space for each individual class in a figure. Args: PopulationModel (class) - as defined in model.py, class containing the population samples, parameters, and a method for evaluating density parameters (list of str) - a subset of parameters of the population model to create a subspace for visualization (must be found in PopulationModel.parameters) plot_samples (bool, optional) - flag for plotting all simulated samples in a scatter plot. Default: False. plot_kdes (bool, optional) - flag for plotting the simulated population KDEs (according to the evaluate_density method specified in PopulationModel) constructed from samples. Default: True. bounds (array-like or None, optional) - pairs of upper and lower bounds for each parameter or None. If provided, should have a shape (N_dim, 2) where N_dim is equal to the number of parameters and has the same order. If bounds are not provided, they are automatically constructed to be 10% of the extent in samples beyond the minimum and maximum value found in samples for each parameter. Default: None. N_bins (int, optional) - Resolution of the grid to evaluate the KDEs on (if plot_kdes=True). Default: 200. N_hist (int, optional) - Resolution of the 1D histogram of samples (if plot_samples=True). Default: 40. levels (int or array-like, optional) - Number and/or positions of contour lines (for >1D KDE plotting). Corresponds to the 'levels' argument in plt.contour. Default: 5. legend (bool) - flag for including plot legend. Default: False. Returns ------- fig, ax (matplotlib objects) - figure visualising population distributions in the specified parameter space """ classes = PopulationModel.classes ndim = len(parameters) fig, ax = plt.subplots() if bounds is None: bounds = get_bounds(PopulationModel, parameters) bins = np.linspace(bounds.T[:][0], bounds.T[:][1], N_bins + 1).T bin_centers = (bins[:, 1:] + bins[:, :-1]) / 2 if ndim == 1: coords_eval = bin_centers elif ndim == 2: X, Y = np.meshgrid(bin_centers[0], bin_centers[1]) coords_eval = np.vstack((X.ravel(), Y.ravel())) else: raise ValueError( "Only plotting 1D and 2D distributions is currently supported." ) for counter, class_name in enumerate(classes): samples = PopulationModel.samples(class_name=class_name, parameters=parameters) if plot_samples: if ndim == 1: ax.hist( samples[:, 0], color=color_cycler[counter % 6], bins=np.linspace(bounds.T[:][0], bounds.T[:][1], N_hist + 1).T[0], alpha=0.5, density=True, label=f"{class_name} samples", ) else: ax.scatter( samples[:, 0], samples[:, 1], color=color_cycler[counter % 6], marker=marker_cycler[counter % 5], edgecolor="black", s=20, label=f"{class_name} samples", ) if plot_kdes: if ndim == 1: eval_ = PopulationModel.evaluate_density( class_name=class_name, parameters=parameters, points=coords_eval.swapaxes(0, 1), ) ax.plot( coords_eval[0], eval_, color=color_cycler[counter % 6], label=f"{class_name} density estimate", ) else: eval_ = PopulationModel.evaluate_density( class_name=class_name, parameters=parameters, points=coords_eval.swapaxes(0, 1), ) ax.contour( X, Y, eval_.reshape(np.shape(X)), colors=color_cycler[counter % 6], linewidths=2, levels=levels, ) legend_proxy = ax.plot( bounds[0] - 1000, bounds[1] - 1000, color=color_cycler[counter % 6], lw=2, label=f"{class_name} density estimate", ) ax.set_xlabel(f"{parameters[0]}") ax.set_xlim(bounds[0]) if ndim == 1: ax.set_ylabel("density") else: ax.set_ylabel(f"{parameters[1]}") ax.set_ylim(bounds[1]) if legend: ax.legend(loc="best", fontsize=10) return fig, ax
[docs] def plot_rel_prob_surfaces( PopulationModel, parameters=None, plot_samples=False, bounds=None, N_bins=1000, create_none_class=None, none_kde=None, none_kde_kwargs={}, ): """ Plots 2D relative probability surfaces (p(class | parameters, model)). A visualisation of probability the classifier would return, for points with exactly known parameters, of belonging to the given class, taking into account distributions and weights of all classes. Args: PopulationModel (class) - as defined in model.py, class containing the population samples, parameters, and a method for evaluating density parameters (list of str) - a subset of parameters of the population model to create a subspace for visualization (must be found in PopulationModel.parameters) plot_samples (bool, optional) - flag for overplotting all simulated samples belonging to a given class. Default: False. bounds (array-like or None, optional) - pairs of upper and lower bounds for each parameter or None. If provided, should have a shape (N_dim, 2) where N_dim is equal to the number of parameters and has the same order. If bounds are not provided, they are automatically constructed to be 10% of the extent in samples beyond the minimum and maximum value found in samples for each parameter. Default: None. N_bins (int, optional) - Resolution of the grid to evaluate the KDEs on. Default: 1000. create_none_class (popclass.NoneClassUQ-like or None, optional) - method to build the 2D None class probability distribution using the grid defined with bounds and N_bins. If None, only classes from PopulationModel are included in visualization. Default: None. none_kde (scipy.stats.gaussian_kde-like, optional) - method to evaluate the overall sample density in the process of building the None class. Passed as the ``kde'' argument when initializing the None class object. Default: None. none_kde_kwargs (dictionary) - extra arguments for evaluating the overall sample density in the process of building the None class. Passed as the ``kde_kwargs'' argument when initializing the None class object. Default: {}. Returns ------- figs, axes (lists of matplotlib objects) - figures visualising relative probability surfaces in the specified parameter space (one for each class) """ classes = PopulationModel.classes ndim = len(parameters) if ndim != 2: raise ValueError( "Only 2-parameter input is currently supported for plotting relative probability surfaces." ) else: if bounds is None: bounds = get_bounds(PopulationModel, parameters) bins = np.linspace(bounds.T[:][0], bounds.T[:][1], N_bins + 1).T bin_centers = (bins[:, 1:] + bins[:, :-1]) / 2 X, Y = np.meshgrid(bin_centers[0], bin_centers[1]) coords_eval = np.vstack((X.ravel(), Y.ravel())) maps_2d = [] weights = [] for class_name in classes: density_eval = PopulationModel.evaluate_density( class_name=class_name, parameters=parameters, points=coords_eval.swapaxes(0, 1), ) map_2d = np.reshape(density_eval, X.shape) weight = PopulationModel.class_weight(class_name) maps_2d.append(map_2d) weights.append(weight) if create_none_class: bounds_dict = {} for counter, parameter in enumerate(parameters): bounds_dict[parameter] = bounds[counter] none_class = create_none_class( bounds=bounds_dict, grid_size=N_bins + 1, population_model=PopulationModel, parameters=parameters, kde=none_kde, kde_kwargs=none_kde_kwargs, ) classes.append("None") map_none = none_class.none_pdf_binned maps_2d.append(map_none) weights.append( none_class.none_class_weight / (1 - none_class.none_class_weight) ) maps_2d, weights = np.array(maps_2d), np.array(weights) weighted_cmaps = (maps_2d.T * weights).T colormaps_normed = weighted_cmaps / np.sum(weighted_cmaps, axis=0) figs, axes = [], [] for counter, class_name in enumerate(classes): fig, ax = plt.subplots() if class_name != "None": cmap = cmap_cycler[counter % 6] if plot_samples: samples = PopulationModel.samples( class_name=class_name, parameters=parameters ) ax.scatter( samples[:, 0], samples[:, 1], color=color_cycler[counter % 6], marker=marker_cycler[counter % 5], edgecolor="black", s=20, label=f"{class_name} samples", ) ax.legend() else: cmap = "Greys" im = ax.imshow( colormaps_normed[counter], origin="lower", extent=[bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1]], cmap=cmap, vmin=0.0, vmax=1.0, ) divider = make_axes_locatable(ax) cax = divider.append_axes("right", size="5%", pad=0.05) cb = fig.colorbar(im, cax=cax, orientation="vertical") ax.set_xlabel(f"{parameters[0]}") ax.set_xlim(bounds[0]) ax.set_ylabel(f"{parameters[1]}") ax.set_ylim(bounds[1]) cb.set_label(f"p({class_name} | " + r"$\phi, \mathcal{G} )$") figs.append(fig) axes.append(ax) return figs, axes