def mean_trace_scatter( y: np.ndarray, trace_color: Tuple[int] = COLORS.STEELBLUE.value, legend_label: str = "mean", hover_labels: Optional[List[str]] = None, ) -> go.Scatter: """Creates a graph object for trace of the mean of the given series across runs. Args: y: (r x t) array with results from r runs and t trials. trace_color: tuple of 3 int values representing an RGB color. Defaults to blue. legend_label: label for this trace. hover_labels: optional, text to show on hover; list where the i-th value corresponds to the i-th value in the value of the `y` argument. Returns: go.Scatter: plotly graph object """ return go.Scatter( name=legend_label, legendgroup=legend_label, x=np.arange(1, y.shape[1] + 1), y=np.mean(y, axis=0), mode="lines", line={"color": rgba(trace_color)}, fillcolor=rgba(trace_color, 0.3), fill="tonexty", text=hover_labels, )
def mean_trace_scatter( y: np.ndarray, trace_color: Tuple[int] = COLORS.STEELBLUE.value, legend_label: str = "mean", ) -> go.Scatter: """Creates a graph object for trace of the mean of the given series across runs. Args: y: (r x t) array with results from r runs and t trials. trace_color: tuple of 3 int values representing an RGB color. Defaults to blue. legend_label: label for this trace Returns: go.Scatter: plotly graph object """ return go.Scatter( name=legend_label, legendgroup=legend_label, x=np.arange(1, y.shape[1] + 1), y=np.mean(y, axis=0), mode="lines", line={"color": rgba(trace_color)}, fillcolor=rgba(trace_color, 0.3), fill="tonexty", )
def model_transitions_scatter( model_transitions: List[int], y_range: List[float], generator_change_color: Tuple[int] = COLORS.TEAL.value, ) -> List[go.Scatter]: """Creates a graph object for the line(s) representing generator changes. Args: model_transitions: iterations, before which generators changed y_range: upper and lower values of the y-range of the plot generator_change_color: tuple of 3 int values representing an RGB color. Defaults to orange. Returns: go.Scatter: plotly graph objects for the lines representing generator changes """ if len(y_range) != 2: # pragma: no cover raise ValueError("y_range should have two values, lower and upper.") data: List[go.Scatter] = [] for change in model_transitions: data.append( go.Scatter( x=[change] * 2, y=y_range, mode="lines", line={ "dash": "dash", "color": rgba(generator_change_color) }, name="Generator change", )) return data
def optimum_objective_scatter( optimum: float, num_iterations: int, optimum_color: Tuple[int] = COLORS.ORANGE.value) -> go.Scatter: """Creates a graph object for the line representing optimal objective. Args: optimum: value of the optimal objective num_iterations: how many trials were in the optimization (used to determine the width of the plot) trace_color: tuple of 3 int values representing an RGB color. Defaults to orange. Returns: go.Scatter: plotly graph objects for the optimal objective line """ return go.Scatter( x=[1, num_iterations], y=[optimum] * 2, mode="lines", line={ "dash": "dash", "color": rgba(optimum_color) }, name="Optimum", )
def mean_markers_scatter( y: np.ndarray, marker_color: Tuple[int] = COLORS.LIGHT_PURPLE.value, legend_label: str = "", hover_labels: Optional[List[str]] = None, ) -> go.Scatter: """Creates a graph object for trace of the mean of the given series across runs, with errorbars. Args: y: (r x t) array with results from r runs and t trials. trace_color: tuple of 3 int values representing an RGB color. Defaults to light purple. legend_label: label for this trace. hover_labels: optional, text to show on hover; list where the i-th value corresponds to the i-th value in the value of the `y` argument. Returns: go.Scatter: plotly graph object """ mean = np.mean(y, axis=0) sem = np.std(y, axis=0) / np.sqrt(y.shape[0]) return go.Scatter( name=legend_label, x=np.arange(1, y.shape[1] + 1), y=mean, error_y={ "type": "data", "array": sem, "visible": True, }, mode="markers", marker={"color": rgba(marker_color)}, text=hover_labels, )
def plot_bandit_rollout(experiment: Experiment) -> AxPlotConfig: """Plot bandit rollout from ane experiement.""" categories: List[str] = [] arms: Dict[str, Dict[str, Any]] = {} data = [] index = 0 for trial in sorted(experiment.trials.values(), key=lambda trial: trial.index): if not isinstance(trial, BatchTrial): raise ValueError( "Bandit rollout graph is not supported for BaseTrial." ) # pragma: no cover category = f"Round {trial.index}" categories.append(category) for arm, weight in trial.normalized_arm_weights(total=100).items(): if arm.name not in arms: arms[arm.name] = { "index": index, "name": arm.name, "x": [], "y": [], "text": [], } index += 1 arms[arm.name]["x"].append(category) arms[arm.name]["y"].append(weight) arms[arm.name]["text"].append("{:.2f}%".format(weight)) for key in arms.keys(): data.append(arms[key]) # pyre-fixme[6]: Expected `typing.Tuple[...g.Tuple[int, int, int]`. colors = [rgba(c) for c in MIXED_SCALE] config = {"data": data, "categories": categories, "colors": colors} return AxPlotConfig(config, plot_type=AxPlotTypes.BANDIT_ROLLOUT)
def sem_range_scatter( y: np.ndarray, trace_color: Tuple[int] = COLORS.STEELBLUE.value, legend_label: str = "", ) -> Tuple[go.Scatter]: """Creates a graph object for trace of mean +/- 2 SEMs for y, across runs. Args: y: (r x t) array with results from r runs and t trials. trace_color: tuple of 3 int values representing an RGB color. Defaults to blue. legend_label: Label for the legend group. Returns: Tuple[go.Scatter]: plotly graph objects for lower and upper bounds """ mean = np.mean(y, axis=0) sem = np.std(y, axis=0) / np.sqrt(y.shape[0]) return ( go. Scatter( # pyre-ignore[16]: `plotly.graph_objs` has no attr. `Scatter` x=np.arange(1, y.shape[1] + 1), y=mean - 2 * sem, legendgroup=legend_label, mode="lines", line={"width": 0}, showlegend=False, hoverinfo="none", ), go. Scatter( # pyre-ignore[16]: `plotly.graph_objs` has no attr. `Scatter` x=np.arange(1, y.shape[1] + 1), y=mean + 2 * sem, legendgroup=legend_label, mode="lines", line={"width": 0}, fillcolor=rgba(trace_color, 0.3), fill="tonexty", showlegend=False, hoverinfo="none", ), )
def scatter_plot_with_pareto_frontier_plotly( Y: np.ndarray, Y_pareto: Optional[np.ndarray], metric_x: Optional[str], metric_y: Optional[str], reference_point: Optional[Tuple[float, float]], minimize: Optional[Union[bool, Tuple[bool, bool]]] = True, ) -> go.Figure: """Plots a scatter of all points in ``Y`` for ``metric_x`` and ``metric_y`` with a reference point and Pareto frontier from ``Y_pareto``. Points in the scatter are colored in a gradient representing their trial index, with metric_x on x-axis and metric_y on y-axis. Reference point is represented as a star and Pareto frontier –– as a line. The frontier connects to the reference point via projection lines. NOTE: Both metrics should have the same minimization setting, passed as `minimize`. Args: Y: Array of outcomes, of which the first two will be plotted. Y_pareto: Array of Pareto-optimal points, first two outcomes in which will be plotted. metric_x: Name of first outcome in ``Y``. metric_Y: Name of second outcome in ``Y``. reference_point: Reference point for ``metric_x`` and ``metric_y``. minimize: Whether the two metrics in the plot are being minimized or maximized. """ title = "Observed metric values" if isinstance(minimize, bool): minimize = (minimize, minimize) Xs = Y[:, 0] Ys = Y[:, 1] experimental_points_scatter = [ go.Scatter( x=Xs, y=Ys, mode="markers", marker={ "color": np.linspace(0, 100, int(len(Xs) * 1.05)), "colorscale": "magma", "colorbar": { "tickvals": [0, 50, 100], "ticktext": [ 1, "iteration", len(Xs), ], }, }, name="Experimental points", ) ] # No Pareto frontier is drawn if none is provided, or if the frontier consists of # a single point and no reference points are provided. if Y_pareto is None or (len(Y_pareto) == 1 and reference_point is None): # `Y_pareto` input was not specified range_x = extend_range(lower=min(Y[:, 0]), upper=max(Y[:, 0])) range_y = extend_range(lower=min(Y[:, 1]), upper=max(Y[:, 1])) pareto_step = reference_point_lines = reference_point_star = [] else: title += " with Pareto frontier" if reference_point: if minimize is None: minimize = tuple(reference_point[i] >= max(Y_pareto[:, i]) for i in range(2)) reference_point_star = [ go.Scatter( x=[reference_point[0]], y=[reference_point[1]], mode="markers", marker={ "color": rgba(COLORS.STEELBLUE.value), "size": 25, "symbol": "star", }, ) ] extra_point_x = min(Y_pareto[:, 0]) if minimize[0] else max( Y_pareto[:, 0]) reference_point_line_1 = go.Scatter( x=[extra_point_x, reference_point[0]], y=[reference_point[1], reference_point[1]], mode="lines", marker={"color": rgba(COLORS.STEELBLUE.value)}, ) extra_point_y = min(Y_pareto[:, 1]) if minimize[1] else max( Y_pareto[:, 1]) reference_point_line_2 = go.Scatter( x=[reference_point[0], reference_point[0]], y=[extra_point_y, reference_point[1]], mode="lines", marker={"color": rgba(COLORS.STEELBLUE.value)}, ) reference_point_lines = [ reference_point_line_1, reference_point_line_2 ] Y_pareto_with_extra = np.concatenate( ( [[extra_point_x, reference_point[1]]], Y_pareto, [[reference_point[0], extra_point_y]], ), axis=0, ) pareto_step = [ go.Scatter( x=Y_pareto_with_extra[:, 0], y=Y_pareto_with_extra[:, 1], mode="lines", line_shape="hv", marker={"color": rgba(COLORS.STEELBLUE.value)}, ) ] range_x = (extend_range(lower=min(Y_pareto[:, 0]), upper=reference_point[0]) if minimize[0] else extend_range(lower=reference_point[0], upper=max(Y_pareto[:, 0]))) range_y = (extend_range(lower=min(Y_pareto[:, 1]), upper=reference_point[1]) if minimize[1] else extend_range(lower=reference_point[1], upper=max(Y_pareto[:, 1]))) else: # Reference point was not specified pareto_step = [ go.Scatter( x=Y_pareto[:, 0], y=Y_pareto[:, 1], mode="lines", line_shape="hv", marker={"color": rgba(COLORS.STEELBLUE.value)}, ) ] reference_point_lines = reference_point_star = [] range_x = extend_range(lower=min(Y_pareto[:, 0]), upper=max(Y_pareto[:, 0])) range_y = extend_range(lower=min(Y_pareto[:, 1]), upper=max(Y_pareto[:, 1])) layout = go.Layout( title=title, showlegend=False, xaxis={ "title": metric_x or "", "range": range_x }, yaxis={ "title": metric_y or "", "range": range_y }, ) return go.Figure( layout=layout, data=pareto_step + reference_point_lines + experimental_points_scatter + reference_point_star, )
def plot_bandit_rollout(experiment: Experiment) -> AxPlotConfig: """Plot bandit rollout from ane experiement.""" categories: List[str] = [] arms: Dict[str, Dict[str, Any]] = {} data = [] index = 0 for trial in sorted(experiment.trials.values(), key=lambda trial: trial.index): if not isinstance(trial, BatchTrial): raise ValueError( "Bandit rollout graph is not supported for BaseTrial." ) # pragma: no cover category = f"Round {trial.index}" categories.append(category) for arm, weight in trial.normalized_arm_weights(total=100).items(): if arm.name not in arms: arms[arm.name] = { "index": index, "name": arm.name, "x": [], "y": [], "text": [], } index += 1 arms[arm.name]["x"].append(category) arms[arm.name]["y"].append(weight) arms[arm.name]["text"].append("{:.2f}%".format(weight)) for key in arms.keys(): data.append(arms[key]) # pyre-fixme[6]: Expected `typing.Tuple[...g.Tuple[int, int, int]`. colors = [rgba(c) for c in MIXED_SCALE] layout = go.Layout( # pyre-ignore[16] title="Rollout Process<br>Bandit Weight Graph", xaxis={ "title": "Rounds", "zeroline": False, "categoryorder": "array", "categoryarray": categories, }, yaxis={"title": "Percent", "showline": False}, barmode="stack", showlegend=False, margin={"r": 40}, ) bandit_config = {"type": "bar", "hoverinfo": "name+text", "width": 0.5} bandits = [ dict(bandit_config, marker={"color": colors[d["index"] % len(colors)]}, **d) for d in data ] for bandit in bandits: del bandit[ "index"] # Have to delete index or figure creation causes error fig = go.Figure(data=bandits, layout=layout) # pyre-ignore[16] return AxPlotConfig(data=fig, plot_type=AxPlotTypes.GENERIC)
def _get_single_pareto_trace( frontier: ParetoFrontierResults, CI_level: float, legend_label: str = "mean", trace_color: Tuple[int] = COLORS.STEELBLUE.value, show_parameterization_on_hover: bool = True, ) -> go.Scatter: primary_means = frontier.means[frontier.primary_metric] primary_sems = frontier.sems[frontier.primary_metric] secondary_means = frontier.means[frontier.secondary_metric] secondary_sems = frontier.sems[frontier.secondary_metric] absolute_metrics = frontier.absolute_metrics all_metrics = frontier.means.keys() if frontier.arm_names is None: arm_names = [ f"Parameterization {i}" for i in range(len(frontier.param_dicts)) ] else: arm_names = [f"Arm {name}" for name in frontier.arm_names] if CI_level is not None: Z = 0.5 * norm.ppf(1 - (1 - CI_level) / 2) else: Z = None labels = [] for i, param_dict in enumerate(frontier.param_dicts): label = f"<b>{arm_names[i]}</b><br>" for metric in all_metrics: metric_lab = _make_label( mean=frontier.means[metric][i], sem=frontier.sems[metric][i], name=metric, is_relative=metric not in absolute_metrics, Z=Z, ) label += metric_lab parameterization = (_format_dict(param_dict, "Parameterization") if show_parameterization_on_hover else "") label += parameterization labels.append(label) return go.Scatter( name=legend_label, legendgroup=legend_label, x=secondary_means, y=primary_means, error_x={ "type": "data", "array": Z * np.array(secondary_sems), "thickness": 2, "color": rgba(trace_color, CI_OPACITY), }, error_y={ "type": "data", "array": Z * np.array(primary_sems), "thickness": 2, "color": rgba(trace_color, CI_OPACITY), }, mode="markers", text=labels, hoverinfo="text", marker={"color": rgba(trace_color)}, )
def optimization_times( fit_times: Dict[str, List[float]], gen_times: Dict[str, List[float]], title: str = "", ) -> AxPlotConfig: """Plots wall times for each method as a bar chart. Args: fit_times: A map from method name to a list of the model fitting times. gen_times: A map from method name to a list of the gen times. title: Title for this plot. Returns: AxPlotConfig with the plot """ # Compute means and SEs methods = list(fit_times.keys()) fit_res: Dict[str, Union[str, List[float]]] = {"name": "Fitting"} fit_res["mean"] = [np.mean(fit_times[m]) for m in methods] fit_res["2sems"] = [ 2 * np.std(fit_times[m]) / np.sqrt(len(fit_times[m])) for m in methods ] gen_res: Dict[str, Union[str, List[float]]] = {"name": "Generation"} gen_res["mean"] = [np.mean(gen_times[m]) for m in methods] gen_res["2sems"] = [ 2 * np.std(gen_times[m]) / np.sqrt(len(gen_times[m])) for m in methods ] total_mean: List[float] = [] total_2sems: List[float] = [] for m in methods: totals = np.array(fit_times[m]) + np.array(gen_times[m]) total_mean.append(np.mean(totals)) total_2sems.append(2 * np.std(totals) / np.sqrt(len(totals))) total_res: Dict[str, Union[str, List[float]]] = { "name": "Total", "mean": total_mean, "2sems": total_2sems, } # Construct plot data: List[go.Bar] = [] for i, res in enumerate([fit_res, gen_res, total_res]): data.append( go.Bar( x=methods, y=res["mean"], text=res["name"], textposition="auto", error_y={ "type": "data", "array": res["2sems"], "visible": True }, marker={ "color": rgba(DISCRETE_COLOR_SCALE[i]), "line": { "color": "rgb(0,0,0)", "width": 1.0 }, }, opacity=0.6, name=res["name"], )) layout = go.Layout( title=title, showlegend=False, yaxis={"title": "Time"}, xaxis={"title": "Method"}, ) return AxPlotConfig(data=go.Figure(layout=layout, data=data), plot_type=AxPlotTypes.GENERIC)
def plot_pareto_frontier( frontier: ParetoFrontierResults, CI_level: float = DEFAULT_CI_LEVEL, show_parameterization_on_hover: bool = True, ) -> AxPlotConfig: """Plot a Pareto frontier from a ParetoFrontierResults object. Args: frontier (ParetoFrontierResults): The results of the Pareto frontier computation. CI_level (float, optional): The confidence level, i.e. 0.95 (95%) show_parameterization_on_hover (bool, optional): If True, show the parameterization of the points on the frontier on hover. Returns: AEPlotConfig: The resulting Plotly plot definition. """ trace = _get_single_pareto_trace( frontier=frontier, CI_level=CI_level, show_parameterization_on_hover=show_parameterization_on_hover, ) shapes = [] primary_threshold = None secondary_threshold = None if frontier.objective_thresholds is not None: primary_threshold = frontier.objective_thresholds.get( frontier.primary_metric, None) secondary_threshold = frontier.objective_thresholds.get( frontier.secondary_metric, None) absolute_metrics = frontier.absolute_metrics rel_x = frontier.secondary_metric not in absolute_metrics rel_y = frontier.primary_metric not in absolute_metrics if primary_threshold is not None: shapes.append({ "type": "line", "xref": "paper", "x0": 0.0, "x1": 1.0, "yref": "y", "y0": primary_threshold, "y1": primary_threshold, "line": { "color": rgba(COLORS.CORAL.value), "width": 3 }, }) if secondary_threshold is not None: shapes.append({ "type": "line", "yref": "paper", "y0": 0.0, "y1": 1.0, "xref": "x", "x0": secondary_threshold, "x1": secondary_threshold, "line": { "color": rgba(COLORS.CORAL.value), "width": 3 }, }) layout = go.Layout( title="Pareto Frontier", xaxis={ "title": frontier.secondary_metric, "ticksuffix": "%" if rel_x else "", "zeroline": True, }, yaxis={ "title": frontier.primary_metric, "ticksuffix": "%" if rel_y else "", "zeroline": True, }, hovermode="closest", legend={"orientation": "h"}, width=750, height=500, margin=go.layout.Margin(pad=4, l=225, b=75, t=75), # noqa E741 shapes=shapes, ) fig = go.Figure(data=[trace], layout=layout) return AxPlotConfig(data=fig, plot_type=AxPlotTypes.GENERIC)
def plot_multiple_pareto_frontiers( frontiers: Dict[str, ParetoFrontierResults], CI_level: float = DEFAULT_CI_LEVEL, show_parameterization_on_hover: bool = True, ) -> AxPlotConfig: """Plot a Pareto frontier from a ParetoFrontierResults object. Args: frontiers (Dict[str, ParetoFrontierResults]): The results of the Pareto frontier computation. CI_level (float, optional): The confidence level, i.e. 0.95 (95%) show_parameterization_on_hover (bool, optional): If True, show the parameterization of the points on the frontier on hover. Returns: AEPlotConfig: The resulting Plotly plot definition. """ first_frontier = list(frontiers.values())[0] traces = [] for i, (method, frontier) in enumerate(frontiers.items()): # Check the two metrics are the same as the first frontier if (frontier.primary_metric != first_frontier.primary_metric or frontier.secondary_metric != first_frontier.secondary_metric): raise ValueError( "All frontiers should have the same pairs of metrics.") trace = _get_single_pareto_trace( frontier=frontier, legend_label=method, trace_color=DISCRETE_COLOR_SCALE[i % len(DISCRETE_COLOR_SCALE)], CI_level=CI_level, show_parameterization_on_hover=show_parameterization_on_hover, ) traces.append(trace) shapes = [] primary_threshold = None secondary_threshold = None if frontier.objective_thresholds is not None: primary_threshold = frontier.objective_thresholds.get( frontier.primary_metric, None) secondary_threshold = frontier.objective_thresholds.get( frontier.secondary_metric, None) absolute_metrics = frontier.absolute_metrics rel_x = frontier.secondary_metric not in absolute_metrics rel_y = frontier.primary_metric not in absolute_metrics if primary_threshold is not None: shapes.append({ "type": "line", "xref": "paper", "x0": 0.0, "x1": 1.0, "yref": "y", "y0": primary_threshold, "y1": primary_threshold, "line": { "color": rgba(COLORS.CORAL.value), "width": 3 }, }) if secondary_threshold is not None: shapes.append({ "type": "line", "yref": "paper", "y0": 0.0, "y1": 1.0, "xref": "x", "x0": secondary_threshold, "x1": secondary_threshold, "line": { "color": rgba(COLORS.CORAL.value), "width": 3 }, }) layout = go.Layout( title="Pareto Frontier", xaxis={ "title": frontier.secondary_metric, "ticksuffix": "%" if rel_x else "", "zeroline": True, }, yaxis={ "title": frontier.primary_metric, "ticksuffix": "%" if rel_y else "", "zeroline": True, }, hovermode="closest", legend={ "orientation": "h", "yanchor": "top", "y": -0.20, "xanchor": "auto", "x": 0.075, }, width=750, height=550, margin=go.layout.Margin(pad=4, l=225, b=125, t=75), # noqa E741 shapes=shapes, ) fig = go.Figure(data=traces, layout=layout) return AxPlotConfig(data=fig, plot_type=AxPlotTypes.GENERIC)
def _error_scatter_trace( arms: List[Union[PlotInSampleArm, PlotOutOfSampleArm]], y_axis_var: PlotMetric, x_axis_var: Optional[PlotMetric] = None, y_axis_label: Optional[str] = None, x_axis_label: Optional[str] = None, status_quo_arm: Optional[PlotInSampleArm] = None, show_CI: bool = True, name: str = "In-sample", color: Tuple[int] = COLORS.STEELBLUE.value, visible: bool = True, legendgroup: Optional[str] = None, showlegend: bool = True, hoverinfo: str = "text", show_arm_details_on_hover: bool = True, show_context: bool = False, arm_noun: str = "arm", ) -> Dict[str, Any]: """Plot scatterplot with error bars. Args: arms (List[Union[PlotInSampleArm, PlotOutOfSampleArm]]): a list of in-sample or out-of-sample arms. In-sample arms have observed data, while out-of-sample arms just have predicted data. As a result, when passing out-of-sample arms, pred must be True. y_axis_var: name of metric for y-axis, along with whether it is observed or predicted. x_axis_var: name of metric for x-axis, along with whether it is observed or predicted. If None, arm names are automatically used. y_axis_label: custom label to use for y axis. If None, use metric name from `y_axis_var`. x_axis_label: custom label to use for x axis. If None, use metric name from `x_axis_var` if that is not None. status_quo_arm: the status quo arm. Necessary for relative metrics. show_CI: if True, plot confidence intervals. name: name of trace. Default is "In-sample". color: color as rgb tuple. Default is (128, 177, 211), which corresponds to COLORS.STEELBLUE. visible: if True, trace is visible (default). legendgroup: group for legends. showlegend: if True, legend if rendered. hoverinfo: information to show on hover. Default is custom text. show_arm_details_on_hover: if True, display parameterizations of arms on hover. Default is True. show_context: if True and show_arm_details_on_hover, context will be included in the hover. arm_noun: noun to use instead of "arm" (e.g. group) """ x, x_se, y, y_se = _error_scatter_data( arms=arms, y_axis_var=y_axis_var, x_axis_var=x_axis_var, status_quo_arm=status_quo_arm, ) labels = [] arm_names = [a.name for a in arms] # No relativization if no x variable. rel_x = x_axis_var.rel if x_axis_var else False rel_y = y_axis_var.rel for i in range(len(arm_names)): heading = f"<b>{arm_noun.title()} {arm_names[i]}</b><br>" x_lab = ("{name}: {estimate}{perc} {ci}<br>".format( name=x_axis_var.metric if x_axis_label is None else x_axis_label, estimate=(round(x[i], DECIMALS) if isinstance( x[i], numbers.Number) else x[i]), ci="" if x_se is None else _format_CI(x[i], x_se[i], rel_x), perc="%" if rel_x else "", ) if x_axis_var is not None else "") y_lab = "{name}: {estimate}{perc} {ci}<br>".format( name=y_axis_var.metric if y_axis_label is None else y_axis_label, estimate=(round(y[i], DECIMALS) if isinstance( y[i], numbers.Number) else y[i]), ci="" if y_se is None else _format_CI(y[i], y_se[i], rel_y), perc="%" if rel_y else "", ) parameterization = (_format_dict(arms[i].parameters, "Parameterization") if show_arm_details_on_hover else "") context = ( # Expected `Dict[str, Optional[Union[bool, float, str]]]` for 1st anonymous # parameter to call `ax.plot.helper._format_dict` but got # `Optional[Dict[str, Union[float, str]]]`. # pyre-fixme[6]: _format_dict(arms[i].context_stratum, "Context") if show_arm_details_on_hover and show_context # noqa W503 and arms[i].context_stratum # noqa W503 else "") labels.append("{arm_name}<br>{xlab}{ylab}{param_blob}{context}".format( arm_name=heading, xlab=x_lab, ylab=y_lab, param_blob=parameterization, context=context, )) i += 1 trace = go.Scatter( x=x, y=y, marker={"color": rgba(color)}, mode="markers", name=name, text=labels, hoverinfo=hoverinfo, ) if show_CI: if x_se is not None: trace.update( error_x={ "type": "data", "array": np.multiply(x_se, Z), "color": rgba(color, CI_OPACITY), }) if y_se is not None: trace.update( error_y={ "type": "data", "array": np.multiply(y_se, Z), "color": rgba(color, CI_OPACITY), }) if visible is not None: trace.update(visible=visible) if legendgroup is not None: trace.update(legendgroup=legendgroup) if showlegend is not None: trace.update(showlegend=showlegend) return trace
def lattice_multiple_metrics( model: ModelBridge, generator_runs_dict: TNullableGeneratorRunsDict = None, rel: bool = True, show_arm_details_on_hover: bool = False, ) -> AxPlotConfig: """Plot raw values or predictions of combinations of two metrics for arms. Args: model: model to draw predictions from. generator_runs_dict: a mapping from generator run name to generator run. rel: if True, use relative effects. Default is True. show_arm_details_on_hover: if True, display parameterizations of arms on hover. Default is False. """ metrics = model.metric_names fig = tools.make_subplots( rows=len(metrics), cols=len(metrics), print_grid=False, shared_xaxes=False, shared_yaxes=False, ) plot_data, _, _ = get_plot_data( model, generator_runs_dict if generator_runs_dict is not None else {}, metrics) status_quo_arm = ( None if plot_data.status_quo_name is None # pyre-fixme[6]: Expected `str` for 1st param but got `Optional[str]`. else plot_data.in_sample.get(plot_data.status_quo_name)) # iterate over all combinations of metrics and generate scatter traces for i, o1 in enumerate(metrics, start=1): for j, o2 in enumerate(metrics, start=1): if o1 != o2: # in-sample observed and predicted obs_insample_trace = _error_scatter_trace( # Expected `List[Union[PlotInSampleArm, # PlotOutOfSampleArm]]` for 1st anonymous parameter to call # `ax.plot.scatter._error_scatter_trace` but got # `List[PlotInSampleArm]`. # pyre-fixme[6]: list(plot_data.in_sample.values()), x_axis_var=PlotMetric(o1, pred=False, rel=rel), y_axis_var=PlotMetric(o2, pred=False, rel=rel), status_quo_arm=status_quo_arm, showlegend=(i == 1 and j == 2), legendgroup="In-sample", visible=False, show_arm_details_on_hover=show_arm_details_on_hover, ) predicted_insample_trace = _error_scatter_trace( # Expected `List[Union[PlotInSampleArm, # PlotOutOfSampleArm]]` for 1st anonymous parameter to call # `ax.plot.scatter._error_scatter_trace` but got # `List[PlotInSampleArm]`. # pyre-fixme[6]: list(plot_data.in_sample.values()), x_axis_var=PlotMetric(o1, pred=True, rel=rel), y_axis_var=PlotMetric(o2, pred=True, rel=rel), status_quo_arm=status_quo_arm, legendgroup="In-sample", showlegend=(i == 1 and j == 2), visible=True, show_arm_details_on_hover=show_arm_details_on_hover, ) fig.append_trace(obs_insample_trace, j, i) fig.append_trace(predicted_insample_trace, j, i) # iterate over models here for k, (generator_run_name, cand_arms) in enumerate( (plot_data.out_of_sample or {}).items(), start=1): fig.append_trace( _error_scatter_trace( list(cand_arms.values()), x_axis_var=PlotMetric(o1, pred=True, rel=rel), y_axis_var=PlotMetric(o2, pred=True, rel=rel), status_quo_arm=status_quo_arm, name=generator_run_name, color=DISCRETE_COLOR_SCALE[k], showlegend=(i == 1 and j == 2), legendgroup=generator_run_name, show_arm_details_on_hover=show_arm_details_on_hover, ), j, i, ) else: # if diagonal is set to True, add box plots fig.append_trace( go.Box( y=[arm.y[o1] for arm in plot_data.in_sample.values()], name=None, marker={"color": rgba(COLORS.STEELBLUE.value)}, showlegend=False, legendgroup="In-sample", visible=False, hoverinfo="none", ), j, i, ) fig.append_trace( go.Box( y=[ arm.y_hat[o1] for arm in plot_data.in_sample.values() ], name=None, marker={"color": rgba(COLORS.STEELBLUE.value)}, showlegend=False, legendgroup="In-sample", hoverinfo="none", ), j, i, ) for k, (generator_run_name, cand_arms) in enumerate( (plot_data.out_of_sample or {}).items(), start=1): fig.append_trace( go.Box( y=[arm.y_hat[o1] for arm in cand_arms.values()], name=None, marker={"color": rgba(DISCRETE_COLOR_SCALE[k])}, showlegend=False, legendgroup=generator_run_name, hoverinfo="none", ), j, i, ) fig["layout"].update( height=800, width=960, font={"size": 10}, hovermode="closest", legend={ "orientation": "h", "x": 0, "y": 1.05, "xanchor": "left", "yanchor": "middle", }, updatemenus=[ { "x": 0.35, "y": 1.08, "xanchor": "left", "yanchor": "middle", "buttons": [ { "args": [{ "error_x.width": 0, "error_x.thickness": 0, "error_y.width": 0, "error_y.thickness": 0, }], "label": "No", "method": "restyle", }, { "args": [{ "error_x.width": 4, "error_x.thickness": 2, "error_y.width": 4, "error_y.thickness": 2, }], "label": "Yes", "method": "restyle", }, ], }, { "x": 0.1, "y": 1.08, "xanchor": "left", "yanchor": "middle", "buttons": [ { "args": [{ "visible": (([False, True] + [True] * len(plot_data.out_of_sample or {})) * (len(metrics)**2)) }], "label": "Modeled", "method": "restyle", }, { "args": [{ "visible": (([True, False] + [False] * len(plot_data.out_of_sample or {})) * (len(metrics)**2)) }], "label": "In-sample", "method": "restyle", }, ], }, ], annotations=[ { "x": 0.02, "y": 1.1, "xref": "paper", "yref": "paper", "text": "Type", "showarrow": False, "yanchor": "middle", "xanchor": "left", }, { "x": 0.30, "y": 1.1, "xref": "paper", "yref": "paper", "text": "Show CI", "showarrow": False, "yanchor": "middle", "xanchor": "left", }, ], ) # add metric names to axes - add to each subplot if boxplots on the # diagonal and axes are not shared; else, add to the leftmost y-axes # and bottom x-axes. for i, o in enumerate(metrics): pos_x = len(metrics) * len(metrics) - len(metrics) + i + 1 pos_y = 1 + (len(metrics) * i) fig["layout"]["xaxis{}".format(pos_x)].update(title=_wrap_metric(o), titlefont={"size": 10}) fig["layout"]["yaxis{}".format(pos_y)].update(title=_wrap_metric(o), titlefont={"size": 10}) # do not put x-axis ticks for boxplots boxplot_xaxes = [] for trace in fig["data"]: if trace["type"] == "box": # stores the xaxes which correspond to boxplot subplots # since we use xaxis1, xaxis2, etc, in plotly.py boxplot_xaxes.append("xaxis{}".format(trace["xaxis"][1:])) else: # clear all error bars since default is no CI trace["error_x"].update(width=0, thickness=0) trace["error_y"].update(width=0, thickness=0) for xaxis in boxplot_xaxes: fig["layout"][xaxis]["showticklabels"] = False return AxPlotConfig(data=fig, plot_type=AxPlotTypes.GENERIC)
def scatter_plot_with_pareto_frontier_plotly( Y: np.ndarray, Y_pareto: np.ndarray, metric_x: str, metric_y: str, reference_point: Tuple[float, float], minimize: bool = True, ) -> go.Figure: """Plots a scatter of all points in ``Y`` for ``metric_x`` and ``metric_y`` with a reference point and Pareto frontier from ``Y_pareto``. Points in the scatter are colored in a gradient representing their trial index, with metric_x on x-axis and metric_y on y-axis. Reference point is represented as a star and Pareto frontier –– as a line. The frontier connects to the reference point via projection lines. NOTE: Both metrics should have the same minimization setting, passed as `minimize`. Args: Y: Array of outcomes, of which the first two will be plotted. Y_pareto: Array of Pareto-optimal points, first two outcomes in which will be plotted. metric_x: Name of first outcome in ``Y``. metric_Y: Name of second outcome in ``Y``. reference_point: Reference point for ``metric_x`` and ``metric_y``. minimize: Whether the two metrics in the plot are being minimized or maximized. """ Xs = Y[:, 0] Ys = Y[:, 1] experimental_points_scatter = go.Scatter( x=Xs, y=Ys, mode="markers", marker={ "color": np.linspace(0, 100, int(len(Xs) * 1.05)), "colorscale": "magma", "colorbar": { "tickvals": [0, 50, 100], "ticktext": [ 1, "iteration", len(Xs), ], }, }, name="Experimental points", ) reference_point_star = go.Scatter( x=[reference_point[0]], y=[reference_point[1]], mode="markers", marker={"color": rgba(COLORS.STEELBLUE.value), "size": 25, "symbol": "star"}, ) extra_point_x = min(Y_pareto[:, 0]) if minimize else max(Y_pareto[:, 0]) reference_point_line_1 = go.Scatter( x=[extra_point_x, reference_point[0]], y=[reference_point[1], reference_point[1]], mode="lines", marker={"color": rgba(COLORS.STEELBLUE.value)}, ) extra_point_y = min(Y_pareto[:, 1]) if minimize else max(Y_pareto[:, 1]) reference_point_line_2 = go.Scatter( x=[reference_point[0], reference_point[0]], y=[extra_point_y, reference_point[1]], mode="lines", marker={"color": rgba(COLORS.STEELBLUE.value)}, ) Y_pareto_with_extra = np.concatenate( ( [[extra_point_x, reference_point[1]]], Y_pareto, [[reference_point[0], extra_point_y]], ), axis=0, ) pareto_step = go.Scatter( x=Y_pareto_with_extra[:, 0], y=Y_pareto_with_extra[:, 1], mode="lines", marker={"color": rgba(COLORS.STEELBLUE.value)}, ) Y_no_outliers = _filter_outliers(Y=Y) range_x = ( extend_range(lower=min(Y_no_outliers[:, 0]), upper=reference_point[0]) if minimize else extend_range(lower=reference_point[0], upper=max(Y_no_outliers[:, 0])) ) range_y = ( extend_range(lower=min(Y_no_outliers[:, 1]), upper=reference_point[1]) if minimize else extend_range(lower=reference_point[1], upper=max(Y_no_outliers[:, 1])) ) layout = go.Layout( title="Observed points with Pareto frontier", showlegend=False, xaxis={"title": metric_x, "range": range_x}, yaxis={"title": metric_y, "range": range_y}, ) return go.Figure( layout=layout, data=[ pareto_step, reference_point_line_1, reference_point_line_2, experimental_points_scatter, reference_point_star, ], )
def plot_pareto_frontier( frontier: ParetoFrontierResults, CI_level: float = DEFAULT_CI_LEVEL, show_parameterization_on_hover: bool = True, ) -> AxPlotConfig: """Plot a Pareto frontier from a ParetoFrontierResults object. Args: frontier (ParetoFrontierResults): The results of the Pareto frontier computation. CI_level (float, optional): The confidence level, i.e. 0.95 (95%) show_parameterization_on_hover (bool, optional): If True, show the parameterization of the points on the frontier on hover. Returns: AEPlotConfig: The resulting Plotly plot definition. """ primary_means = frontier.means[frontier.primary_metric] primary_sems = frontier.sems[frontier.primary_metric] secondary_means = frontier.means[frontier.secondary_metric] secondary_sems = frontier.sems[frontier.secondary_metric] absolute_metrics = frontier.absolute_metrics all_metrics = frontier.means.keys() if frontier.arm_names is None: arm_names = [ f"Parameterization {i}" for i in range(len(frontier.param_dicts)) ] else: arm_names = [f"Arm {name}" for name in frontier.arm_names] if CI_level is not None: Z = 0.5 * norm.ppf(1 - (1 - CI_level) / 2) else: Z = None primary_threshold = None secondary_threshold = None if frontier.objective_thresholds is not None: primary_threshold = frontier.objective_thresholds.get( frontier.primary_metric, None) secondary_threshold = frontier.objective_thresholds.get( frontier.secondary_metric, None) labels = [] rel_x = frontier.secondary_metric not in absolute_metrics rel_y = frontier.primary_metric not in absolute_metrics for i, param_dict in enumerate(frontier.param_dicts): label = f"<b>{arm_names[i]}</b><br>" for metric in all_metrics: metric_lab = _make_label( mean=frontier.means[metric][i], sem=frontier.sems[metric][i], name=metric, is_relative=metric not in absolute_metrics, Z=Z, ) label += metric_lab parameterization = (_format_dict(param_dict, "Parameterization") if show_parameterization_on_hover else "") label += parameterization labels.append(label) traces = [ go.Scatter( x=secondary_means, y=primary_means, error_x={ "type": "data", "array": Z * np.array(secondary_sems), "thickness": 2, "color": rgba(COLORS.STEELBLUE.value, CI_OPACITY), }, error_y={ "type": "data", "array": Z * np.array(primary_sems), "thickness": 2, "color": rgba(COLORS.STEELBLUE.value, CI_OPACITY), }, mode="markers", text=labels, hoverinfo="text", ) ] shapes = [] if primary_threshold is not None: shapes.append({ "type": "line", "xref": "paper", "x0": 0.0, "x1": 1.0, "yref": "y", "y0": primary_threshold, "y1": primary_threshold, "line": { "color": rgba(COLORS.CORAL.value), "width": 3 }, }) if secondary_threshold is not None: shapes.append({ "type": "line", "yref": "paper", "y0": 0.0, "y1": 1.0, "xref": "x", "x0": secondary_threshold, "x1": secondary_threshold, "line": { "color": rgba(COLORS.CORAL.value), "width": 3 }, }) layout = go.Layout( title="Pareto Frontier", xaxis={ "title": frontier.secondary_metric, "ticksuffix": "%" if rel_x else "", "zeroline": True, }, yaxis={ "title": frontier.primary_metric, "ticksuffix": "%" if rel_y else "", "zeroline": True, }, hovermode="closest", legend={"orientation": "h"}, width=750, height=500, margin=go.layout.Margin(pad=4, l=225, b=75, t=75), # noqa E741 shapes=shapes, ) fig = go.Figure(data=traces, layout=layout) return AxPlotConfig(data=fig, plot_type=AxPlotTypes.GENERIC)