Source code for estimagic.visualization.derivative_plot

"""Visualize and compare derivative estimates."""

import itertools

import numpy as np
import pandas as pd
import plotly.graph_objects as go

from estimagic.config import PLOTLY_PALETTE, PLOTLY_TEMPLATE
from estimagic.visualization.plotting_utilities import create_grid_plot, create_ind_dict


[docs]def derivative_plot( derivative_result, combine_plots_in_grid=True, template=PLOTLY_TEMPLATE, palette=PLOTLY_PALETTE, ): """Plot evaluations and derivative estimates. The resulting grid plot displays function evaluations and derivatives. The derivatives are visualized as a first-order Taylor approximation. Bands are drawn indicating the area in which forward and backward derivatives are located. This is done by filling the area between the derivative estimate with lowest and highest step size, respectively. Do not confuse these bands with statistical errors. This function does not require the params vector as plots are displayed relative to the point at which the derivative is calculated. Args: derivative_result (dict): The result dictionary of call to :func:`~estimagic.differentiation.derivatives.first_derivative` with return_info and return_func_value set to True. combine_plots_in_grid (bool): decide whether to return a one figure containing subplots for each factor pair or a dictionary of individual plots. Default True. template (str): The template for the figure. Default is "plotly_white". palette: The coloring palette for traces. Default is "qualitative.Plotly". Returns: plotly.Figure: The grid plot or dict of individual plots """ func_value = derivative_result["func_value"] func_evals = derivative_result["func_evals"] derivative_candidates = derivative_result["derivative_candidates"] # remove index from main data for plotting df = func_evals.reset_index() df = df.assign(step=df.step * df.sign) func_evals = df.set_index(["sign", "step_number", "dim_x", "dim_f"]) # prepare derivative data df_der = _select_derivative_with_minimal_error(derivative_candidates) df_der_method = _select_derivative_with_minimal_error( derivative_candidates, given_method=True ) # auxiliary grid_points = 2 # we do not need more than 2 grid points since all lines are affine func_value = np.atleast_1d(func_value) max_steps = df.groupby("dim_x")["step"].max() # dimensions of params vector (dim_x) span the vertical axis while dimensions of # output (dim_f) span the horizontal axis of produced figure dim_x = range(df["dim_x"].max() + 1) dim_f = range(df["dim_f"].max() + 1) # plotting # container for titles titles = [] # container for x-axis titles x_axis = [] # container for individual plots g_list = [] # creating data traces for plotting faceted/individual plots for row, col in itertools.product(dim_x, dim_f): g_ind = [] # container for data for traces in individual plot # initial values and x grid y0 = func_value[col] x_grid = np.linspace(-max_steps[row], max_steps[row], grid_points) # initial values and x grid y0 = func_value[col] x_grid = np.linspace(-max_steps[row], max_steps[row], grid_points) # function evaluations scatter points _scatter_data = func_evals.query("dim_x == @row & dim_f == @col") trace_func_evals = go.Scatter( x=_scatter_data["step"], y=_scatter_data["eval"], mode="markers", name="Function Evaluation", legendgroup=1, marker={"color": "black"}, ) g_ind.append(trace_func_evals) # best derivative estimate given each method for i, method in enumerate(["forward", "central", "backward"]): _y = y0 + x_grid * df_der_method.loc[method, row, col] trace_method = go.Scatter( x=x_grid, y=_y, mode="lines", name=method, legendgroup=2 + i, line={"color": palette[i], "width": 5}, ) g_ind.append(trace_method) # fill area for sign, cmap_id in zip([1, -1], [0, 2]): # cmap_id of ['forward', 'backward'] _x_y = _select_eval_with_lowest_and_highest_step(func_evals, sign, row, col) diff = _x_y - np.array([0, y0]) slope = diff[:, 1] / diff[:, 0] _y = y0 + x_grid * slope.reshape(-1, 1) trace_fill_lines = go.Scatter( x=x_grid, y=_y[0, :], mode="lines", line={"color": palette[cmap_id], "width": 1}, showlegend=False, ) g_ind.append(trace_fill_lines) trace_fill_area = go.Scatter( x=x_grid, y=_y[1, :], mode="lines", line={"color": palette[cmap_id], "width": 1}, fill="tonexty", ) g_ind.append(trace_fill_area) # overall best derivative estimate _y = y0 + x_grid * df_der.loc[row, col] trace_best_estimate = go.Scatter( x=x_grid, y=_y, mode="lines", name="Best Estimate", legendgroup=2, line={"color": "black", "width": 2}, ) g_ind.append(trace_best_estimate) # subplot x titles x_axis.append(rf"Value relative to x<sub>{0, row}</sub>") # subplot titles titles.append(f"dim_x, dim_f = {row, col}") # list of traces for individual plots g_list.append(g_ind) common_dependencies = { "ind_list": g_list, "names": titles, "clean_legend": True, "scientific_notation": True, "x_title": x_axis, } common_layout = { "template": template, "margin": {"l": 10, "r": 10, "t": 30, "b": 10}, } # Plot with subplots if combine_plots_in_grid: g = create_grid_plot( rows=len(dim_x), cols=len(dim_f), **common_dependencies, kws={ "height": 300 * len(dim_x), "width": 500 * len(dim_f), **common_layout, }, ) out = g # Dictionary for individual plots else: ind_dict = create_ind_dict( **common_dependencies, kws={"height": 300, "width": 500, "title_x": 0.5, **common_layout}, ) out = ind_dict return out
def _select_derivative_with_minimal_error(df_jac_cand, given_method=False): """Select derivatives with minimal error component wise. Args: df_jac_cand (pandas.DataFrame): Frame containing jacobian candidates. given_method (bool): Boolean indicating wether to condition on columns method in df_jac_cand. Default is False, which selects the overall best derivative estimate. Returns: df (pandas.DataFrame): The (best) derivative estimate. """ given = ["method"] if given_method else [] minimizer = df_jac_cand.groupby([*given, "dim_x", "dim_f"])["err"].idxmin() df = df_jac_cand.loc[minimizer]["der"] index_level_to_drop = list({"method", "num_term"} - set(given)) df = df.droplevel(index_level_to_drop).copy() return df def _select_eval_with_lowest_and_highest_step(df_evals, sign, dim_x, dim_f): """Select step and eval from data with highest and lowest step. Args: df_evals (pd.DataFrame): Frame containing func evaluations (long-format). sign (int): Direction of step. dim_x (int): Dimension of x to select. dim_f (int): Dimension of f to select. Returns: out (numpy.ndarray): Array of shape (2, 2). Columns correspond to step and eval, while rows correspond to lowest and highest step, respectively. """ df = df_evals.loc[(sign, slice(None), dim_x, dim_f), ["step", "eval"]] df = df.dropna().sort_index() out = pd.concat([df.head(1), df.tail(1)]).to_numpy() return out