"""
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