Source code for gunz_ml.plots.contour

"""
Plotting function for visualizing 2D contour plots of hyperparameters vs. an objective.
"""
# =============================================================================
# METADATA
# =============================================================================
__author__ = "Yeremia Gunawan Adhisantoso"
__email__ = "adhisant@tnt.uni-hannover.de"
__license__ = "Clear BSD"
__version__ = "1.0.0"

# =============================================================================
# STANDARD LIBRARY IMPORTS
# =============================================================================
import itertools
import logging
import pathlib
import typing as t

# =============================================================================
# THIRD-PARTY IMPORTS
# =============================================================================
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from omegaconf import DictConfig
from scipy.interpolate import griddata

# =============================================================================
# LOCAL APPLICATION IMPORTS
# =============================================================================
from .utils import DEFAULT_PLOT_CONFIG

[docs] def plot_contour( #? --- Data Inputs --- metric_df: pd.DataFrame, param_df: pd.DataFrame, #? --- Plotting Configuration --- objective_metrics: t.List[str], output_path: pathlib.Path, continuous_params_map: t.Dict[str, str], plot_cfg: t.Union[DictConfig, t.Dict], ) -> None: """ Generates contour plots of 2 continuous parameters vs. 1 objective. """ logging.info("--- Starting Contour Plot Generation ---") if len(metric_df) < 15: logging.warning("Skipping contour plots: not enough trials (< 15) for interpolation.") return if len(continuous_params_map) < 2: logging.warning("Skipping contour plots: need at least 2 continuous parameters.") return plot_output_path = output_path / "continuous_contour" cfg = {**DEFAULT_PLOT_CONFIG, **plot_cfg} for param_x, param_y in itertools.combinations(continuous_params_map.keys(), 2): for metric_z in objective_metrics: try: points = param_df[[param_x, param_y]].values values = metric_df[metric_z].values is_x_log, is_y_log = (continuous_params_map[param_x] == 'log'), (continuous_params_map[param_y] == 'log') points_for_grid = np.copy(points).astype(float) if is_x_log: points_for_grid[:, 0] = np.log10(points_for_grid[:, 0]) if is_y_log: points_for_grid[:, 1] = np.log10(points_for_grid[:, 1]) grid_x, grid_y = np.mgrid[ points_for_grid[:,0].min():points_for_grid[:,0].max():100j, points_for_grid[:,1].min():points_for_grid[:,1].max():100j ] grid_z = griddata(points_for_grid, values, (grid_x, grid_y), method='cubic') x_coords = 10**grid_x[:,0] if is_x_log else grid_x[:,0] y_coords = 10**grid_y[0,:] if is_y_log else grid_y[0,:] fig = go.Figure(data=[go.Contour( x=x_coords, y=y_coords, z=grid_z.T, colorscale='Turbo', contours=dict(coloring='heatmap', showlabels=True, labelfont=dict(size=10, color='white')) )]) fig.add_trace(go.Scatter( x=param_df[param_x], y=param_df[param_y], mode='markers', marker=dict(color='rgba(255,255,255,0.5)', size=4), showlegend=False, hovertemplate=f'<b>{param_x}</b>: %{{x}}<br><b>{param_y}</b>: %{{y}}<extra></extra>' )) fig.update_layout( title=f'Contour: {param_x} vs. {param_y}<br><sub>on {metric_z}</sub>', xaxis_title=f"{param_x} ({'log' if is_x_log else 'linear'})", yaxis_title=f"{param_y} ({'log' if is_y_log else 'linear'})", xaxis_type='log' if is_x_log else 'linear', yaxis_type='log' if is_y_log else 'linear', template=cfg['template'] ) param_name = f"{param_x}_vs_{param_y}" param_output_path = plot_output_path / param_name param_output_path.mkdir(parents=True, exist_ok=True) fig.write_html(param_output_path / f"{metric_z.replace('/', '_')}.html") except Exception as e: logging.warning(f"Could not generate contour for ({param_x}, {param_y}) vs {metric_z}: {e}") continue