Source code for optuna.visualization.matplotlib._parallel_coordinate

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

from optuna._experimental import experimental_func
from optuna.visualization._parallel_coordinate import _get_parallel_coordinate_info
from optuna.visualization._parallel_coordinate import _ParallelCoordinateInfo
from optuna.visualization.matplotlib._matplotlib_imports import _imports


if TYPE_CHECKING:
    from collections.abc import Callable
    from typing import Literal

    from optuna.study import Study
    from optuna.trial import FrozenTrial


if _imports.is_successful():
    from optuna.visualization.matplotlib._matplotlib_imports import Axes
    from optuna.visualization.matplotlib._matplotlib_imports import BoundaryNorm
    from optuna.visualization.matplotlib._matplotlib_imports import Line2D
    from optuna.visualization.matplotlib._matplotlib_imports import LineCollection
    from optuna.visualization.matplotlib._matplotlib_imports import plt


[docs] @experimental_func("2.2.0") def plot_parallel_coordinate( study: Study, params: list[str] | None = None, *, target: Callable[[FrozenTrial], float] | None = None, target_name: str = "Objective Value", ) -> "Axes": """Plot the high-dimensional parameter relationships in a study with Matplotlib. If a trial does not contain a parameter, the line is connected to a special ``None`` tick. If at least one completed trial contains constraint values, feasible trials are drawn with solid lines and infeasible trials with dotted lines. In this case, trials without constraint values are treated as infeasible. .. seealso:: Please refer to :func:`optuna.visualization.plot_parallel_coordinate` for an example. Args: study: A :class:`~optuna.study.Study` object whose trials are plotted for their target values. params: Parameter list to visualize. The default is all parameters. target: A function to specify the value to display. If it is :obj:`None` and ``study`` is being used for single-objective optimization, the objective values are plotted. For a multi-objective study, all objective values are plotted and the lines are colored by Pareto rank. Pareto ranks are computed from the objective values without considering constraints. target_name: Target's name to display on the axis label and the legend. This is used for single-objective studies or when ``target`` is specified, and ignored for multi-objective studies when ``target`` is :obj:`None`. Returns: A :class:`matplotlib.axes.Axes` object. .. note:: The colormap is reversed when the ``target`` argument isn't :obj:`None` or ``direction`` of :class:`~optuna.study.Study` is ``minimize``. The Pareto-rank colormap is always reversed for multi-objective studies. """ _imports.check() info = _get_parallel_coordinate_info(study, params, target, target_name) return _get_parallel_coordinate_plot(info)
def _get_parallel_coordinate_plot(info: _ParallelCoordinateInfo) -> "Axes": # _get_parallel_coordinate_info always provides at least one objective dimension. plot_dimensions = info.dims_objectives[1:] + info.dims_params first_objective_dim = info.dims_objectives[0] dimensions = info.dims_objectives + info.dims_params # Set up the graph style. fig, ax = plt.subplots() cmap = plt.get_cmap("Blues_r" if info.reverse_scale else "Blues") ax.set_title("Parallel Coordinate Plot", y=1.03) ax.spines["top"].set_visible(False) ax.spines["bottom"].set_visible(False) # Prepare data for plotting. if len(plot_dimensions) == 0 or len(first_objective_dim.values) == 0: return ax obj_min = first_objective_dim.range[0] obj_max = first_objective_dim.range[1] objective_is_constant = obj_min == obj_max if objective_is_constant: padding = abs(obj_min) * 0.05 if obj_min != 0.0 else 0.5 obj_min -= padding obj_max += padding obj_w = obj_max - obj_min draw_order = info.draw_order dims_obj_base = [[first_objective_dim.values[i]] for i in draw_order] for dim in plot_dimensions: p_min = dim.range[0] p_max = dim.range[1] p_w = p_max - p_min if p_w == 0.0: center = obj_w / 2 + obj_min for row in dims_obj_base: row.append(center) else: for row, trial_id in zip(dims_obj_base, draw_order): value = dim.values[trial_id] row.append((value - p_min) / p_w * obj_w + obj_min) # Draw multiple line plots and axes. # Ref: https://stackoverflow.com/a/50029441 n_params = len(plot_dimensions) ax.set_xlim(0, n_params) ax.set_ylim(obj_min, obj_max) if objective_is_constant: ax.set_yticks([first_objective_dim.range[0]]) xs = [range(n_params + 1) for _ in range(len(dims_obj_base))] segments = [np.column_stack([x, y]) for x, y in zip(xs, dims_obj_base)] lc = LineCollection(segments, cmap=cmap) if info.feasibility is not None: lc.set_linestyle( ["solid" if info.feasibility[trial_id] else "dotted" for trial_id in draw_order] ) feasibility_legend: list[tuple[bool, str, Literal["solid", "dotted"]]] = [ (True, "Feasible", "solid"), (False, "Infeasible", "dotted"), ] legend_handles = [ Line2D([0], [0], color="#2F3B52", linestyle=linestyle, label=label) for _, label, linestyle in feasibility_legend ] ax.legend( handles=legend_handles, loc="lower right", bbox_to_anchor=(1.0, 1.02), ncol=len(legend_handles), frameon=False, ) color_values = tuple(np.asarray(info.color_values)[list(info.draw_order)]) lc.set_array(np.asarray(color_values)) if info.is_rank_color: max_rank = int(max(color_values)) lc.set_norm(BoundaryNorm(np.arange(-0.5, max_rank + 1.5), cmap.N)) axcb = fig.colorbar(lc, pad=0.1, ax=ax) if info.is_rank_color: axcb.set_ticks(list(range(int(max(color_values)) + 1))) axcb.set_label(info.colorbar_title) var_names = [dim.label for dim in dimensions] plt.xticks(range(n_params + 1), var_names, rotation=330) for i, dim in enumerate(plot_dimensions): ax2 = ax.twinx() if dim.is_log and not dim.has_missing: ax2.set_ylim(np.power(10, dim.range[0]), np.power(10, dim.range[1])) ax2.set_yscale("log") else: ax2.set_ylim(dim.range[0], dim.range[1]) ax2.spines["top"].set_visible(False) ax2.spines["bottom"].set_visible(False) ax2.xaxis.set_visible(False) ax2.spines["right"].set_position(("axes", (i + 1) / n_params)) if dim.is_cat or dim.has_missing: ax2.set_yticks(dim.tickvals) ax2.set_yticklabels(dim.ticktext) ax.add_collection(lc) return ax