def contour_plot_bokeh( model, xlabel=None, ylabel=None, main=None, xlim=(-3.2, 3.2), ylim=(-3.2, 3.2), colour_function="terrain", show=True, show_expt_data=True, figsize=(10, 10), dpi=50, other_factors=None, ): # TODO: show labels of contour plot # https://stackoverflow.com/questions/33533047/how-to-make-a-contour-plot-in-python-using-bokeh-or-other-libs dpi_max = dpi**3.5 N = min(dpi, np.power(dpi_max, 0.5)) h_grid = np.linspace(xlim[0], xlim[1], num=N) v_grid = np.linspace(ylim[0], ylim[1], num=N) H, V = np.meshgrid(h_grid, v_grid) h_grid, v_grid = H.ravel(), V.ravel() pure_factors = model.get_factor_names(level=1) if xlabel is None: xlabel = pure_factors[0] else: xlabel = str(xlabel) if ylabel is None: ylabel = pure_factors[1] else: ylabel = str(ylabel) kwargs = {xlabel: h_grid, ylabel: v_grid} if other_factors is not None and isinstance(other_factors, dict): kwargs = kwargs.update(other_factors) # Look at which factors are included, and pop them out. The remaining # factors are specified at their zero level unspecified_factors = [i for i in pure_factors if i not in kwargs.keys()] for factor in unspecified_factors: kwargs[factor] = np.zeros_like(h_grid) assert sorted(kwargs.keys()) == sorted(pure_factors), ("Not all factors " "were specified.") Z = predict(model, **kwargs) Z = Z.values.reshape(N, N) z_min, z_max = Z.min(), Z.max() levels = np.linspace(z_min, z_max, N) from matplotlib.pyplot import contour, clabel import matplotlib.pyplot as plt import matplotlib matplotlib.use("Agg") # Turn interactive plotting off plt.ioff() CS = contour(H, V, Z, levels=levels, linestyles="dotted") clabel(CS, inline=True, fontsize=10, fmt="%1.0f") # contour_labels = [(float(q._x), float(q._y), float(q._text))\ # for q in CS.labelTexts] # Convert the Matplotlib colour mapper to Bokeh # https://stackoverflow.com/questions/49931311/using-matplotlibs-colormap-for-bokehs-color-bar mapper = getattr(cm, colour_function) colours = (255 * mapper(range(256))).astype("int") colour_palette = [RGB(*tuple(rgb)).to_hex() for rgb in colours] color_mapper = LinearColorMapper(palette=colour_palette, low=z_min, high=z_max) # Another alternative: # https://stackoverflow.com/questions/35315259/using-colormap-with-bokeh-scatter # colors = ["#%02x%02x%02x" % (int(r), int(g), int(b)) for \ # r, g, b, _ in 255*mpl.cm.viridis(mpl.colors.Normalize()(radii))] p = figure( x_range=xlim, y_range=ylim, # https://github.com/bokeh/bokeh/issues/2351 tools="pan,wheel_zoom,box_zoom,box_select,lasso_select,reset,save", ) # Create the image layer source = {"Xax": [h_grid], "Yax": [v_grid], "predictions": [Z]} h_image = p.image( source=source, image="predictions", x=xlim[0], y=ylim[0], dw=xlim[1] - xlim[0], dh=ylim[1] - ylim[0], color_mapper=color_mapper, global_alpha=0.5, # with some transparency name="contour_image", ) h1 = HoverTool( tooltips=[ (xlabel, "@{Xax}{0.4g}"), (ylabel, "@{Yax}{0.4f}"), ("Predicted", "@{predictions}{0.4g}"), ], renderers=[h_image], formatters={ "Predicted": "printf", xlabel: "printf", ylabel: "printf" }, ) color_bar = ColorBar( color_mapper=color_mapper, major_label_text_font_size="8pt", ticker=BasicTicker(max_interval=(z_max - z_min) / N * 2), formatter=PrintfTickFormatter(format="%.2f"), label_standoff=6, border_line_color=None, location=(0, 0), ) p.add_layout(color_bar, "right") # Contour lines using Scipy: # scaler_y = (ylim[1] - ylim[0]) / (N - 1) # scaler_x = (xlim[1] - xlim[0]) / (N - 1) # for level in levels: # contours = measure.find_contours(Z, level) # for contour in contours: # x = contour[:, 1] * scaler_y + ylim[0] # y = contour[:, 0] * scaler_x + xlim[0] for _, cccontour in enumerate(CS.allsegs): if cccontour: x = cccontour[0][:, 0] y = cccontour[0][:, 1] p.line(x, y, line_dash="dashed", color="darkgrey", line_width=1) # TODO: bigger experimental markers # TODO: hover for the data point shows the factor settings for the data point if show_expt_data: source = ColumnDataSource(data=dict( x=model.data[xlabel], y=model.data[ylabel], output=model.data[model.get_response_name()].to_list(), )) h_expts = p.circle( x="x", y="y", color="black", source=source, # linestyle='', # marker='o', size=10, line_width=2, name="experimental_points", ) # custom tooltip for the experimental points h2 = HoverTool( tooltips=[ (xlabel, "$x{0.4g}"), (ylabel, "$y{0.4g}"), ("Actual value", "@{output}{0.4g}"), # why not working??? ], renderers=[h_expts], formatters={ "Actual value": "printf", xlabel: "printf", ylabel: "printf" }, ) h2.point_policy = "snap_to_data" h2.line_policy = "none" # Axis labels: p.xaxis.axis_label_text_font_size = "14pt" p.xaxis.axis_label = xlabel p.xaxis.major_label_text_font_size = "14pt" p.xaxis.axis_label_text_font_style = "bold" p.xaxis.bounds = (xlim[0], xlim[1]) p.yaxis.major_label_text_font_size = "14pt" p.yaxis.axis_label = ylabel p.yaxis.axis_label_text_font_size = "14pt" p.yaxis.axis_label_text_font_style = "bold" p.yaxis.bounds = (ylim[0], ylim[1]) # Add the hover tooltips: p.add_tools(h1) p.add_tools(h2) if show: show_plot(p) return p
def plot_model( model, x_column, y_column=None, fig=None, x_slider=None, y_slider=None, show_expt_data=True, figsize=(10, 10), dpi=100, **kwargs, ): """ Plots a `model` object with a given model input as `x_column` against the model's output: `y_column`. If `y_column` is not specified, then it found from the `model`. For model's with more than 1 inputs, the `y_column` is the variable to be plotted on the y-axis, and then the plot type is a contour plot. """ pure_factors = model.get_factor_names(level=1) dpi_max = dpi**3.5 # should be reasonable for most modern computers per_axis_points = min(dpi, np.power(dpi_max, 1 / len(pure_factors))) # `oneD=True`: the x-variable is a model input, and the y-axis is a response oneD = False if y_column and y_column not in pure_factors: oneD = True y_column = model.get_response_name() param_names = [ model.get_response_name(), ] param_names.extend(model.get_factor_names()) # Not always a great test: y = I(1/d) does not pick up that "d" is the model # even though it is via the term encapsulated in I(...) # assert x_column in param_names, "x_column must exist in the model." assert y_column in param_names, "y_column must exist in the model." xrange = model.data[x_column].min(), model.data[x_column].max() xdelta = xrange[1] - xrange[0] xlim = kwargs.get("xlim", (xrange[0] - xdelta * 0.05, xrange[1] + xdelta * 0.05)) h_grid = np.linspace(xlim[0], xlim[1], num=per_axis_points) plotdata = {x_column: h_grid} if not oneD: yrange = model.data[y_column].min(), model.data[y_column].max() ydelta = yrange[1] - yrange[0] ylim = kwargs.get( "ylim", (yrange[0] - ydelta * 0.05, yrange[1] + ydelta * 0.05)) v_grid = np.linspace(ylim[0], ylim[1], num=per_axis_points) H, V = np.meshgrid(h_grid, v_grid) h_grid, v_grid = H.ravel(), V.ravel() plotdata[x_column] = h_grid plotdata[y_column] = v_grid # TODO: handle the 2D case later # if other_factors is not None and isinstance(other_factors, dict): # plotdata = kwargs.update(other_factors) ## Look at which factors are included, and pop them out. The remaining ## factors are specified at their zero level # unspecified_factors = [i for i in pure_factors if i not in kwargs.keys()] # for factor in unspecified_factors: # plotdata[factor] = np.zeros_like(h_grid) # assert sorted(kwargs.keys()) == sorted(pure_factors), ("Not all factors " # "were specified.") Z = predict(model, **plotdata) if not oneD: assert False else: plotdata[y_column] = Z yrange = Z.min(), Z.max() ydelta = yrange[1] - yrange[0] ylim = kwargs.get( "ylim", (yrange[0] - ydelta * 0.05, yrange[1] + ydelta * 0.05)) if fig: p = fig prior_figure = True else: prior_figure = False p = figure( x_range=xlim, y_range=ylim, # https://github.com/bokeh/bokeh/issues/2351 tools="pan,wheel_zoom,box_zoom, box_select,lasso_select,reset,save", ) h_line = p.line( plotdata[x_column], plotdata[y_column], line_dash="solid", color=kwargs.get("color", "black"), line_width=kwargs.get("line_width", 2), ) y_units = model.data.pi_units[y_column] tooltips = [(x_column, "$x")] if y_units: tooltips.append((f"Prediction of {y_column} [{y_units}]", "$y")) else: tooltips.append((f"Prediction of {y_column}", "$y")) tooltips.append(("Source", model.name or "")) # custom tooltip for the predicted prediction line h1 = HoverTool(tooltips=tooltips, renderers=[h_line]) h1.line_policy = "nearest" if show_expt_data: source = ColumnDataSource(data=dict( x=model.data[x_column], y=model.data[y_column], output=model.data[model.get_response_name()].to_list(), )) h_expts = p.circle( x="x", y="y", color="black", source=source, size=10, line_width=2, name="Experimental_points", ) h2 = HoverTool( tooltips=[ (x_column, "$x"), (y_column, "$y"), ("Experimental value", "@output"), ], renderers=[h_expts], ) h2.point_policy = "snap_to_data" h2.line_policy = "none" # Axis labels: p.xaxis.axis_label_text_font_size = "14pt" p.xaxis.axis_label = x_column p.xaxis.major_label_text_font_size = "14pt" p.xaxis.axis_label_text_font_style = "bold" p.yaxis.major_label_text_font_size = "14pt" p.yaxis.axis_label = y_column p.yaxis.axis_label_text_font_size = "14pt" p.yaxis.axis_label_text_font_style = "bold" if prior_figure: # p.xaxis.bounds = p.x_range = Range1d( min(xlim[0], p.x_range.start, min(model.data[x_column])), max(xlim[1], p.x_range.end, max(model.data[x_column])), ) p.y_range = Range1d( min(ylim[0], p.y_range.start, min(model.data[y_column])), max(ylim[1], p.y_range.end, max(model.data[y_column])), ) else: p.x_range = Range1d(xlim[0], xlim[1]) p.y_range = Range1d(ylim[0], ylim[1]) # Add the hover tooltips: p.add_tools(h1) p.add_tools(h2) show_plot(p) return p
def pareto_plot( model, ylabel="Effect name", xlabel="Magnitude of effect", # show all factors and interactions up_to_level=None, main="Pareto plot", legendtitle="Sign of coefficients", negative=("Negative effects", "#0080FF"), positive=("Positive effects", "#FF8000"), show=True, plot_width=500, plot_height=None, # In the hover over the bars aliasing_up_to_level=2, ): # TODO: show full variable names + units on pareto plot for main factors # an interactions """ Plots the Pareto plot for a given model. Parameters ---------- model: required; a model created by the package. ylabel: string; optional, default: "Effect name" The label on the y-axis of the Pareto plot. xlabel: string; optional, default: "Magnitude of effect" The label on the x-axis of the Pareto plot up_to_level: integer, default = None [all levels] Up to which level interactions should be displayed main: string; optional, default: "Pareto plot" The plot title. legendtitle: string; optional, default: "Sign of coefficients" The legend's title. negative: tuple; optional, default: ("Negative", "grey") The first entry is the legend text for negative coefficients, and the second entry is the colour of the negative coefficient bars. positive: tuple; optional, default: ("Positive", "black") The first entry is the legend text for positive coefficients, and the second entry is the colour of the positive coefficient bars. show: boolean; optional, default: True Whether or not to show the plot directly. aliasing_up_to_level: int; optional, default: 2 Shows aliasing as hover entries on the bars, up to this level of interaction. Returns ------- The plot handle. Can be further manipulated, e.g. for saving. Example ------- model = linear() pareto_plot(model, main="Pareto plot for my experiment") p = pareto_plot(model, main="Pareto plot for my experiment", show=False) p.save('save_plot_to_figure.png') """ # TODO: show error bars : see Bokeh annotations: Whiskers model # p.add_layout( # Whisker(source=e_source, base="base", upper="upper", lower="lower") # ) # error_bars = model._OLS.conf_int() # http://holoviews.org/reference/elements/bokeh/ErrorBars.html # https://docs.bokeh.org/en/latest/docs/user_guide/annotations.html params = model.get_parameters() if up_to_level: assert isinstance(up_to_level, int), ("Specify an integer value for " "`up_to_level`.") keep = [] for k in range(up_to_level): keep.extend(model.get_factor_names(level=k + 1)) params = params.filter(keep) param_values = params.values beta_str = [f"+{i:0.4g}" if i > 0 else f"{i:0.4g}" for i in param_values] bar_colours = [negative[1] if p < 0 else positive[1] for p in param_values] bar_signs = ["Positive" if i > 0 else "Negative" for i in param_values] # Show the absolute parameter values, but we will colour code them params = params.abs() base_parameters = model.get_factor_names(level=1) full_names = [] for param_name, _ in params.iteritems(): if param_name in base_parameters: fname = model.data.pi_source.get(param_name, param_name) full_names.append(fname) else: full_names.append(f"Interaction between {param_name}") # Shuffle the collected information in the same way sort_order = params.argsort().values beta_str = [beta_str[i] for i in sort_order] bar_colours = [bar_colours[i] for i in sort_order] bar_signs = [bar_signs[i] for i in sort_order] full_names = [full_names[i] for i in sort_order] TOOLTIPS = [ ("Short name", "@factor_names"), ("Full name", "@full_names"), ("Magnitude and sign", "@original_magnitude_with_sign"), ] alias_strings = model.get_aliases(aliasing_up_to_level, drop_intercept=True, websafe=True) if len(alias_strings) != 0: TOOLTIPS.append(("Aliasing", "@alias_strings{safe}"), ) else: alias_strings = [""] * len(params.values) alias_strings = [alias_strings[i] for i in sort_order] # And only right at the end you can sort the parameter values: params = params.sort_values(na_position="last") source = ColumnDataSource(data=dict( x=params.values, y=np.arange(1, len(params.index) + 1), factor_names=params.index.values, bar_colours=bar_colours, bar_signs=bar_signs, full_names=full_names, original_magnitude_with_sign=beta_str, alias_strings=alias_strings, )) p = figure( plot_width=plot_width, plot_height=plot_height or (500 + (len(params) - 8) * 20), tooltips=TOOLTIPS, title=get_plot_title(main, model, prefix="Pareto plot"), ) p.hbar( y="y", right="x", height=0.5, left=0, fill_color="bar_colours", line_color="bar_colours", legend="bar_signs", source=source, ) p.xaxis.axis_label_text_font_size = "14pt" p.xaxis.axis_label = xlabel p.xaxis.major_label_text_font_size = "14pt" p.xaxis.axis_label_text_font_style = "normal" p.xaxis.bounds = (0, params.max() * 1.05) p.yaxis.major_label_text_font_size = "14pt" p.yaxis.axis_label = ylabel p.yaxis.axis_label_text_font_size = "14pt" p.yaxis.axis_label_text_font_style = "normal" locations = source.data["y"].tolist() labels = source.data["factor_names"] p.yaxis.ticker = locations p.yaxis.major_label_overrides = dict(zip(locations, labels)) p.legend.orientation = "vertical" p.legend.location = "bottom_right" if show: show_plot(p) else: return p