def _generate_contour_subplot( trials: List[FrozenTrial], x_param: str, y_param: str, reverse_scale: bool, param_values_range: Optional[Dict[str, Tuple[float, float]]] = None, target: Optional[Callable[[FrozenTrial], float]] = None, target_name: str = "Objective Value", ) -> Tuple["Contour", "Scatter"]: if param_values_range is None: param_values_range = {} x_indices = sorted(set(_get_param_values(trials, x_param))) y_indices = sorted(set(_get_param_values(trials, y_param))) if len(x_indices) < 2: _logger.warning( "Param {} unique value length is less than 2.".format(x_param)) return go.Contour(), go.Scatter() if len(y_indices) < 2: _logger.warning( "Param {} unique value length is less than 2.".format(y_param)) return go.Contour(), go.Scatter() # Padding to the plot for non-categorical params. x_range = param_values_range[x_param] if _is_numerical(trials, x_param): x_indices = [x_range[0]] + x_indices + [x_range[1]] y_range = param_values_range[y_param] if _is_numerical(trials, y_param): y_indices = [y_range[0]] + y_indices + [y_range[1]] z = [[float("nan") for _ in range(len(x_indices))] for _ in range(len(y_indices))] x_values = [] y_values = [] for trial in trials: if x_param not in trial.params or y_param not in trial.params: continue x_value = trial.params[x_param] y_value = trial.params[y_param] if not _is_numerical(trials, x_param): x_value = str(x_value) if not _is_numerical(trials, y_param): y_value = str(y_value) x_values.append(x_value) y_values.append(y_value) x_i = x_indices.index(x_value) y_i = y_indices.index(y_value) if target is None: value = trial.value else: value = target(trial) if isinstance(value, int): value = float(value) elif not isinstance(value, float): raise ValueError( f"Trial{trial.number} has COMPLETE state, but its target value is non-numeric." ) z[y_i][x_i] = value contour = go.Contour( x=x_indices, y=y_indices, z=z, colorbar={"title": target_name}, colorscale=COLOR_SCALE, connectgaps=True, contours_coloring="heatmap", hoverinfo="none", line_smoothing=1.3, reversescale=reverse_scale, ) scatter = go.Scatter( x=x_values, y=y_values, marker={ "line": { "width": 2.0, "color": "Grey" }, "color": "black" }, mode="markers", showlegend=False, ) return (contour, scatter)
def _get_contour_plot( study: Study, params: Optional[List[str]] = None, target: Optional[Callable[[FrozenTrial], float]] = None, target_name: str = "Objective Value", ) -> "go.Figure": layout = go.Layout(title="Contour Plot") trials = _filter_nonfinite(study.get_trials( deepcopy=False, states=(TrialState.COMPLETE, )), target=target) if len(trials) == 0: _logger.warning("Your study does not have any completed trials.") return go.Figure(data=[], layout=layout) all_params = {p_name for t in trials for p_name in t.params.keys()} if params is None: sorted_params = sorted(all_params) elif len(params) <= 1: _logger.warning("The length of params must be greater than 1.") return go.Figure(data=[], layout=layout) else: for input_p_name in params: if input_p_name not in all_params: raise ValueError( "Parameter {} does not exist in your study.".format( input_p_name)) sorted_params = sorted(set(params)) padding_ratio = 0.05 param_values_range = {} for p_name in sorted_params: values = _get_param_values(trials, p_name) min_value = min(values) max_value = max(values) if _is_log_scale(trials, p_name): padding = (math.log10(max_value) - math.log10(min_value)) * padding_ratio min_value = math.pow(10, math.log10(min_value) - padding) max_value = math.pow(10, math.log10(max_value) + padding) elif _is_numerical(trials, p_name): padding = (max_value - min_value) * padding_ratio min_value = min_value - padding max_value = max_value + padding else: # Plotly>=4.12.0 draws contours using the indices of categorical variables instead of # raw values and the range should be updated based on the cardinality of categorical # variables. See https://github.com/optuna/optuna/issues/1967. if version.parse(plotly.__version__) >= version.parse("4.12.0"): span = len(set(values)) - 1 padding = span * padding_ratio min_value = -padding max_value = span + padding param_values_range[p_name] = (min_value, max_value) reverse_scale = _is_reverse_scale(study, target) if len(sorted_params) == 2: x_param = sorted_params[0] y_param = sorted_params[1] sub_plots = _generate_contour_subplot(trials, x_param, y_param, reverse_scale, param_values_range, target, target_name) figure = go.Figure(data=sub_plots, layout=layout) figure.update_xaxes(title_text=x_param, range=param_values_range[x_param]) figure.update_yaxes(title_text=y_param, range=param_values_range[y_param]) if not _is_numerical(trials, x_param): figure.update_xaxes(type="category") if not _is_numerical(trials, y_param): figure.update_yaxes(type="category") if _is_log_scale(trials, x_param): log_range = [math.log10(p) for p in param_values_range[x_param]] figure.update_xaxes(range=log_range, type="log") if _is_log_scale(trials, y_param): log_range = [math.log10(p) for p in param_values_range[y_param]] figure.update_yaxes(range=log_range, type="log") else: figure = make_subplots(rows=len(sorted_params), cols=len(sorted_params), shared_xaxes=True, shared_yaxes=True) figure.update_layout(layout) showscale = True # showscale option only needs to be specified once for x_i, x_param in enumerate(sorted_params): for y_i, y_param in enumerate(sorted_params): if x_param == y_param: figure.add_trace(go.Scatter(), row=y_i + 1, col=x_i + 1) else: sub_plots = _generate_contour_subplot( trials, x_param, y_param, reverse_scale, param_values_range, target, target_name, ) contour = sub_plots[0] scatter = sub_plots[1] contour.update( showscale=showscale) # showscale's default is True if showscale: showscale = False figure.add_trace(contour, row=y_i + 1, col=x_i + 1) figure.add_trace(scatter, row=y_i + 1, col=x_i + 1) figure.update_xaxes(range=param_values_range[x_param], row=y_i + 1, col=x_i + 1) figure.update_yaxes(range=param_values_range[y_param], row=y_i + 1, col=x_i + 1) if not _is_numerical(trials, x_param): figure.update_xaxes(type="category", row=y_i + 1, col=x_i + 1) if not _is_numerical(trials, y_param): figure.update_yaxes(type="category", row=y_i + 1, col=x_i + 1) if _is_log_scale(trials, x_param): log_range = [ math.log10(p) for p in param_values_range[x_param] ] figure.update_xaxes(range=log_range, type="log", row=y_i + 1, col=x_i + 1) if _is_log_scale(trials, y_param): log_range = [ math.log10(p) for p in param_values_range[y_param] ] figure.update_yaxes(range=log_range, type="log", row=y_i + 1, col=x_i + 1) if x_i == 0: figure.update_yaxes(title_text=y_param, row=y_i + 1, col=x_i + 1) if y_i == len(sorted_params) - 1: figure.update_xaxes(title_text=x_param, row=y_i + 1, col=x_i + 1) return figure
def _generate_contour_subplot( trials: List[FrozenTrial], x_param: str, y_param: str, ax: "Axes", cmap: "Colormap", contour_point_num: int, target: Optional[Callable[[FrozenTrial], float]], ) -> "ContourSet": x_indices = sorted(set(_get_param_values(trials, x_param))) y_indices = sorted(set(_get_param_values(trials, y_param))) if len(x_indices) < 2: _logger.warning("Param {} unique value length is less than 2.".format(x_param)) return ax if len(y_indices) < 2: _logger.warning("Param {} unique value length is less than 2.".format(y_param)) return ax ( xi, yi, zi, x_values, y_values, x_values_range, y_values_range, x_cat_param_pos, x_cat_param_label, y_cat_param_pos, y_cat_param_label, x_values_dummy_count, y_values_dummy_count, ) = _calculate_griddata( trials, x_param, x_indices, y_param, y_indices, contour_point_num, target ) cs = None ax.set(xlabel=x_param, ylabel=y_param) ax.set_xlim(x_values_range[0], x_values_range[1]) ax.set_ylim(y_values_range[0], y_values_range[1]) if len(zi) > 0: if _is_log_scale(trials, x_param): ax.set_xscale("log") if _is_log_scale(trials, y_param): ax.set_yscale("log") if x_param != y_param: # Contour the gridded data. ax.contour(xi, yi, zi, 15, linewidths=0.5, colors="k") cs = ax.contourf(xi, yi, zi, 15, cmap=cmap.reversed()) # Plot data points. if x_values_dummy_count > 0: x_org_len = int(len(x_values) / (x_values_dummy_count + 1)) y_org_len = int(len(y_values) / (x_values_dummy_count + 1)) elif y_values_dummy_count > 0: x_org_len = int(len(x_values) / (y_values_dummy_count + 1)) y_org_len = int(len(y_values) / (y_values_dummy_count + 1)) else: x_org_len = len(x_values) y_org_len = len(x_values) ax.scatter( x_values[:x_org_len], y_values[:y_org_len], marker="o", c="black", s=20, edgecolors="grey", linewidth=2.0, ) if x_cat_param_pos: ax.set_xticks(x_cat_param_pos) ax.set_xticklabels(x_cat_param_label) if y_cat_param_pos: ax.set_yticks(y_cat_param_pos) ax.set_yticklabels(y_cat_param_label) ax.label_outer() return cs