From 8192cc8d2f58ed66f5e464d2ab9146dc672357b0 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Thu, 8 Apr 2021 21:58:02 -0500 Subject: [PATCH 1/8] feat: Add plot_ratio API * Add .plot_ratio to BastHist API - Adds ratio plot support for other Hists and callables * Factor out plot_ratio and plot_pull to perform the final subplot plotting - Majority of logic is factored out of plot_pull into _plot_ratiolike - _plot_ratiolike then calls plot_ratio or plot_pull as needed --- src/hist/basehist.py | 21 +- src/hist/plot.py | 457 +++++++++++++++++++++++++++++++------------ 2 files changed, 357 insertions(+), 121 deletions(-) diff --git a/src/hist/basehist.py b/src/hist/basehist.py index 179921f4..ca1c09ec 100644 --- a/src/hist/basehist.py +++ b/src/hist/basehist.py @@ -401,6 +401,23 @@ def plot2d_full( return hist.plot.plot2d_full(self, ax_dict=ax_dict, **kwargs) + def plot_ratio( + self, + other: Callable[[np.ndarray], np.ndarray], + *, + ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, + **kwargs: Any, + ) -> "Tuple[matplotlib.axes.Axes, matplotlib.axes.Axes]": + """ + plot_ratio method for BaseHist object. + """ + + import hist.plot + + return hist.plot._plot_ratiolike( + self, other, ax_dict=ax_dict, view="ratio", **kwargs + ) + def plot_pull( self, func: Callable[[np.ndarray], np.ndarray], @@ -414,7 +431,9 @@ def plot_pull( import hist.plot - return hist.plot.plot_pull(self, func, ax_dict=ax_dict, **kwargs) + return hist.plot._plot_ratiolike( + self, func, ax_dict=ax_dict, view="pull", **kwargs + ) def plot_pie( self, diff --git a/src/hist/plot.py b/src/hist/plot.py index 2cec5e03..62c86470 100644 --- a/src/hist/plot.py +++ b/src/hist/plot.py @@ -1,11 +1,14 @@ import inspect import sys -from typing import Any, Callable, Dict, Iterable, Optional, Set, Tuple, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union import numpy as np import hist +from .intervals import ratio_uncertainty +from .typing import Literal + try: import matplotlib.axes import matplotlib.patches as patches @@ -20,7 +23,14 @@ raise -__all__ = ("histplot", "hist2dplot", "plot2d_full", "plot_pull", "plot_pie") +__all__ = ( + "histplot", + "hist2dplot", + "plot2d_full", + "plot_ratio", + "plot_pull", + "plot_pie", +) def __dir__() -> Tuple[str, ...]: @@ -214,19 +224,247 @@ def plot2d_full( return main_art, top_art, side_art +def _construct_gaussian_callable( + _hist: hist.BaseHist, +) -> Callable[[np.ndarray, float, float, float], np.ndarray]: + x_values = _hist.axes[0].centers + hist_values = _hist.values() + + # gaussian with reasonable initial guesses for parameters + constant = float(hist_values.max()) + mean = (hist_values * x_values).sum() / hist_values.sum() + sigma = (hist_values * np.square(x_values - mean)).sum() / hist_values.sum() + + def gauss( + x: np.ndarray, + constant: float = constant, + mean: float = mean, + sigma: float = sigma, + ) -> Any: + # Note: As return is a numpy ufuncs the type is "Any" + return constant * np.exp(-np.square(x - mean) / (2 * np.square(sigma))) + + return gauss + + +def _fit_callable_to_hist( + model: Callable[[np.ndarray], np.ndarray], + _hist: hist.BaseHist, + likelihood: bool = False, +) -> "Tuple[np.ndarray, np.ndarray, np.ndarray, Tuple[Tuple[float, ...], np.ndarray]]": + """ + Fit a model, a callable function, to the histogram values. + """ + variances = _hist.variances() + if variances is None: + raise RuntimeError( + "Cannot compute from a variance-less histogram, try a Weight storage" + ) + hist_uncert = np.sqrt(variances) + + # Infer best fit model parameters and covariance matrix + xdata = _hist.axes[0].centers + popt, pcov = _curve_fit_wrapper( + model, xdata, _hist.values(), hist_uncert, likelihood=likelihood + ) + model_values = model(xdata, *popt) + + if np.isfinite(pcov).all(): + n_samples = 100 + vopts = np.random.multivariate_normal(popt, pcov, n_samples) + sampled_ydata = np.vstack([model(xdata, *vopt).T for vopt in vopts]) + model_uncert = np.nanstd(sampled_ydata, axis=0) + else: + model_uncert = np.zeros_like(hist_uncert) + + return model_values, model_uncert, hist_uncert, (popt, pcov) + + +def _plot_fit_result( + _hist: hist.BaseHist, + model_values: np.ndarray, + model_uncert: np.ndarray, + ax: matplotlib.axes.Axes, + eb_kwargs: Dict[str, Any], + fp_kwargs: Dict[str, Any], + ub_kwargs: Dict[str, Any], +) -> List[matplotlib.artist.Artist]: + """ + Plot fit of model to histogram data + """ + x_values = _hist.axes[0].centers + variances = _hist.variances() + if variances is None: + raise RuntimeError( + "Cannot compute from a variance-less histogram, try a Weight storage" + ) + hist_uncert = np.sqrt(variances) + + _errorbars = ax.errorbar(x_values, _hist.values(), hist_uncert, **eb_kwargs) + + # Ensure zorder draws data points above model + line_zorder = _errorbars[0].get_zorder() - 1 + (line,) = ax.plot(x_values, model_values, **fp_kwargs, zorder=line_zorder) + + # Uncertainty band for fitted function + # TODO: Probably set a better default color than the fit line color + ub_kwargs.setdefault("color", line.get_color()) + ax.fill_between( + x_values, + model_values - model_uncert, + model_values + model_uncert, + **ub_kwargs, + ) + + return ax.get_children() + + +def plot_ratio( + _hist: hist.BaseHist, + ratio: np.ndarray, + ratio_uncert: np.ndarray, + ax: matplotlib.axes.Axes, + **kwargs: Any, +) -> matplotlib.axes.Axes: + """ + Plot a ratio plot on the given axes + """ + x_values = _hist.axes[0].centers + left_edge = _hist.axes.edges[0][0] + right_edge = _hist.axes.edges[-1][-1] + + # Set 0 and inf to nan to hide during plotting + ratio[ratio == 0] = np.nan + ratio[np.isinf(ratio)] = np.nan + + central_value = kwargs.pop("central_value", 1.0) + ax.axhline(central_value, color="black", linestyle="dashed", linewidth=1.0) + + uncert_draw_type = kwargs.pop("uncert_draw_type", "line") + if uncert_draw_type == "line": + ax.errorbar( + x_values, + ratio, + yerr=ratio_uncert, + color="black", + marker="o", + linestyle="none", + ) + elif uncert_draw_type == "bar": + bar_width = (right_edge - left_edge) / len(ratio) + + bar_top = ratio + ratio_uncert[1] + bar_bottom = ratio - ratio_uncert[0] + # bottom can't be nan + bar_bottom[np.isnan(bar_bottom)] = 0 + bar_height = bar_top - bar_bottom + + _ratio_points = ax.scatter(x_values, ratio, color="black") + + # Ensure zorder draws data points above uncertainty bars + bar_zorder = _ratio_points.get_zorder() - 1 + ax.bar( + x_values, + height=bar_height, + width=bar_width, + bottom=bar_bottom, + fill=False, + linewidth=0, + edgecolor="gray", + hatch=3 * "/", + zorder=bar_zorder, + ) + + ratio_ylim = kwargs.pop("ylim", None) + if ratio_ylim is None: + # plot centered around central value with a scaled view range + # the value _with_ the uncertainty in view is important so base + # view range on extrema of value +/- uncertainty + valid_ratios_idx = np.where(np.isnan(ratio) == False) # noqa: E712 + valid_ratios = ratio[valid_ratios_idx] + extrema = np.array( + [ + valid_ratios - ratio_uncert[0][valid_ratios_idx], + valid_ratios + ratio_uncert[1][valid_ratios_idx], + ] + ) + max_delta = np.max(np.abs(extrema - central_value)) + ratio_extrema = np.abs(max_delta + central_value) + + _alpha = 2.0 + scaled_offset = max_delta + (max_delta / (_alpha * ratio_extrema)) + ratio_ylim = [central_value - scaled_offset, central_value + scaled_offset] + + ax.set_xlim(left_edge, right_edge) + ax.set_ylim(bottom=ratio_ylim[0], top=ratio_ylim[1]) + + ax.set_xlabel(_hist.axes[0].label) + ax.set_ylabel(kwargs.pop("ylabel", "Ratio")) + + return ax + + def plot_pull( + _hist: hist.BaseHist, + pulls: np.ndarray, + ax: matplotlib.axes.Axes, + bar_kwargs: Dict[str, Any], + pp_kwargs: Dict[str, Any], +) -> matplotlib.axes.Axes: + """ + Plot a pull plot on the given axes + """ + x_values = _hist.axes[0].centers + left_edge = _hist.axes.edges[0][0] + right_edge = _hist.axes.edges[-1][-1] + + # Pull: plot the pulls using Matplotlib bar method + width = (right_edge - left_edge) / len(pulls) + ax.bar(x_values, pulls, width=width, **bar_kwargs) + + pp_num = pp_kwargs.pop("num", 5) + patch_height = max(np.abs(pulls)) / pp_num + patch_width = width * len(pulls) + for i in range(pp_num): + # gradient color patches + if "alpha" in pp_kwargs: + pp_kwargs["alpha"] *= np.power(0.618, i) + else: + pp_kwargs["alpha"] = 0.5 * np.power(0.618, i) + + upRect_startpoint = (left_edge, i * patch_height) + upRect = patches.Rectangle( + upRect_startpoint, patch_width, patch_height, **pp_kwargs + ) + ax.add_patch(upRect) + downRect_startpoint = (left_edge, -(i + 1) * patch_height) + downRect = patches.Rectangle( + downRect_startpoint, patch_width, patch_height, **pp_kwargs + ) + ax.add_patch(downRect) + + ax.set_xlim(left_edge, right_edge) + + ax.set_xlabel(_hist.axes[0].label) + ax.set_ylabel("Pull") + + return ax + + +def _plot_ratiolike( self: hist.BaseHist, - func: Union[Callable[[np.ndarray], np.ndarray], str], + other: Union[hist.BaseHist, Callable[[np.ndarray], np.ndarray]], likelihood: bool = False, *, ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, + view: Literal["ratio", "pull"], fit_fmt: Optional[str] = None, **kwargs: Any, ) -> "Tuple[matplotlib.axes.Axes, matplotlib.axes.Axes]": r""" - Plot_pull method for BaseHist object. + Plot ratio-like plots (ratio plots and pull plots) for BaseHist - fit_fmt can be a string such as r"{name} = {value:.3g} $\pm$ {error:.3g}" + ``fit_fmt`` can be a string such as ``r"{name} = {value:.3g} $\pm$ {error:.3g}"`` """ try: @@ -234,23 +472,24 @@ def plot_pull( from scipy.optimize import curve_fit # noqa: F401 except ModuleNotFoundError: print( - "Hist.plot_pull requires scipy and iminuit. Please install hist[plot] or manually install dependencies.", + f"Hist.plot_{view} requires scipy and iminuit. Please install hist[plot] or manually install dependencies.", file=sys.stderr, ) raise - # Type judgement - if not callable(func) and not type(func) in [str]: - msg = f"Parameter func must be callable or a string for {self.__class__.__name__} in plot pull" - raise TypeError(msg) - if self.ndim != 1: - raise TypeError("Only 1D-histogram supports pull plot, try projecting to 1D") + raise TypeError( + f"Only 1D-histogram supports ratio plot, try projecting {self.__class__.__name__} to 1D" + ) + if isinstance(other, hist.hist.Hist) and other.ndim != 1: + raise TypeError( + f"Only 1D-histogram supports ratio plot, try projecting other={other.__class__.__name__} to 1D" + ) if ax_dict: try: main_ax = ax_dict["main_ax"] - pull_ax = ax_dict["pull_ax"] + subplot_ax = ax_dict[f"{view}_ax"] except KeyError: raise ValueError("All axes should be all given or none at all") else: @@ -258,141 +497,119 @@ def plot_pull( grid = fig.add_gridspec(2, 1, hspace=0, height_ratios=[3, 1]) main_ax = fig.add_subplot(grid[0]) - pull_ax = fig.add_subplot(grid[1], sharex=main_ax) + subplot_ax = fig.add_subplot(grid[1], sharex=main_ax) plt.setp(main_ax.get_xticklabels(), visible=False) - # Computation and Fit - xdata = self.axes[0].centers - ydata = self.values() - variances = self.variances() - if variances is None: - raise RuntimeError( - "Cannot compute from a variance-less histogram, try a Weight storage" - ) - yerr = np.sqrt(variances) - - if isinstance(func, str): - if func in {"gauss", "gaus"}: - # gaussian with reasonable initial guesses for parameters - constant = float(ydata.max()) - mean = (ydata * xdata).sum() / ydata.sum() - sigma = (ydata * (xdata - mean) ** 2.0).sum() / ydata.sum() - - def func( - x: np.ndarray, - constant: float = constant, - mean: float = mean, - sigma: float = sigma, - ) -> np.ndarray: - return constant * np.exp(-((x - mean) ** 2.0) / (2 * sigma ** 2)) # type: ignore - - else: - func = _expr_to_lambda(func) - - assert not isinstance(func, str) - - parnames = list(inspect.signature(func).parameters)[1:] - - # Compute fit values: using func as fit model - popt, pcov = _curve_fit_wrapper(func, xdata, ydata, yerr, likelihood=likelihood) - perr = np.diag(pcov) ** 0.5 - yfit = func(self.axes[0].centers, *popt) - - if np.isfinite(pcov).all(): - nsamples = 100 - vopts = np.random.multivariate_normal(popt, pcov, nsamples) - sampled_ydata = np.vstack([func(xdata, *vopt).T for vopt in vopts]) - yfiterr = np.nanstd(sampled_ydata, axis=0) - else: - yfiterr = np.zeros_like(yerr) - - # Compute pulls: containing no INF values - with np.errstate(divide="ignore"): - pulls = (ydata - yfit) / yerr - - pulls[np.isnan(pulls)] = 0 - pulls[np.isinf(pulls)] = 0 - # Keyword Argument Conversion: convert the kwargs to several independent args - # error bar keyword arguments eb_kwargs = _filter_dict(kwargs, "eb_") eb_kwargs.setdefault("label", "Histogram Data") + # Use "fmt" over "marker" to avoid UserWarning on keyword precedence eb_kwargs.setdefault("fmt", "o") + eb_kwargs.setdefault("linestyle", "none") # fit plot keyword arguments - label = "Fit" - if fit_fmt is not None: - for name, value, error in zip(parnames, popt, perr): - label += "\n " - label += fit_fmt.format(name=name, value=value, error=error) fp_kwargs = _filter_dict(kwargs, "fp_") - fp_kwargs.setdefault("label", label) + fp_kwargs.setdefault("label", "Counts") + + # bar plot keyword arguments + bar_kwargs = _filter_dict(kwargs, "bar_", ignore={"bar_width"}) # uncertainty band keyword arguments ub_kwargs = _filter_dict(kwargs, "ub_") ub_kwargs.setdefault("label", "Uncertainty") - ub_kwargs.setdefault("alpha", 0.5) - # bar plot keyword arguments - bar_kwargs = _filter_dict(kwargs, "bar_", ignore={"bar_width"}) + # ratio plot keyword arguments + rp_kwargs = _filter_dict(kwargs, "rp_") + rp_kwargs.setdefault("uncertainty_type", "poisson") + rp_kwargs.setdefault("legend_loc", "best") + rp_kwargs.setdefault("num_label", None) + rp_kwargs.setdefault("denom_label", None) # patch plot keyword arguments - pp_kwargs = _filter_dict(kwargs, "pp_", ignore={"pp_num"}) - pp_num = kwargs.pop("pp_num", 5) + pp_kwargs = _filter_dict(kwargs, "pp_") # Judge whether some arguments are left if kwargs: raise ValueError(f"{set(kwargs)}' not needed") - # Main: plot the pulls using Matplotlib errorbar and plot methods - main_ax.errorbar(self.axes.centers[0], ydata, yerr, **eb_kwargs) + main_ax.set_ylabel(fp_kwargs["label"]) - (line,) = main_ax.plot(self.axes.centers[0], yfit, **fp_kwargs) - - # Uncertainty band - ub_kwargs.setdefault("color", line.get_color()) - main_ax.fill_between( - self.axes.centers[0], - yfit - yfiterr, - yfit + yfiterr, - **ub_kwargs, - ) - main_ax.legend(loc=0) - main_ax.set_ylabel("Counts") - - # Pull: plot the pulls using Matplotlib bar method - left_edge = self.axes.edges[0][0] - right_edge = self.axes.edges[-1][-1] - width = (right_edge - left_edge) / len(pulls) - pull_ax.bar(self.axes.centers[0], pulls, width=width, **bar_kwargs) - - patch_height = max(np.abs(pulls)) / pp_num - patch_width = width * len(pulls) - for i in range(pp_num): - # gradient color patches - if "alpha" in pp_kwargs: - pp_kwargs["alpha"] *= np.power(0.618, i) + # Computation and Fit + hist_values = self.values() + + if callable(other) or isinstance(other, str): + if isinstance(other, str): + if other in {"gauss", "gaus", "normal"}: + other = _construct_gaussian_callable(self) + else: + other = _expr_to_lambda(other) + + ( + compare_values, + model_uncert, + hist_values_uncert, + bestfit_result, + ) = _fit_callable_to_hist(other, self, likelihood) + + if fit_fmt is not None: + parnames = list(inspect.signature(other).parameters)[1:] + popt, pcov = bestfit_result + perr = np.sqrt(np.diag(pcov)) + + fp_label = "Fit" + for name, value, error in zip(parnames, popt, perr): + fp_label += "\n " + fp_label += fit_fmt.format(name=name, value=value, error=error) + fp_kwargs["label"] = fp_label else: - pp_kwargs["alpha"] = 0.5 * np.power(0.618, i) - - upRect_startpoint = (left_edge, i * patch_height) - upRect = patches.Rectangle( - upRect_startpoint, patch_width, patch_height, **pp_kwargs - ) - pull_ax.add_patch(upRect) - downRect_startpoint = (left_edge, -(i + 1) * patch_height) - downRect = patches.Rectangle( - downRect_startpoint, patch_width, patch_height, **pp_kwargs + fp_kwargs["label"] = "Fitted value" + + # TODO FIXME + # main_ax = _plot_fit_result( + _plot_fit_result( + self, + model_values=compare_values, + model_uncert=model_uncert, + ax=main_ax, + eb_kwargs=eb_kwargs, + fp_kwargs=fp_kwargs, + ub_kwargs=ub_kwargs, ) - pull_ax.add_patch(downRect) + else: + compare_values = other.values() + + histplot(self, ax=main_ax, label=rp_kwargs["num_label"]) + histplot(other, ax=main_ax, label=rp_kwargs["denom_label"]) + + # Compute ratios: containing no INF values + with np.errstate(divide="ignore", invalid="ignore"): + if view == "ratio": + ratios = hist_values / compare_values + ratio_uncert = ratio_uncertainty( + num=hist_values, + denom=compare_values, + uncertainty_type=rp_kwargs["uncertainty_type"], + ) + # ratio: plot the ratios using Matplotlib errorbar or bar + subplot_ax = plot_ratio( + self, ratios, ratio_uncert, ax=subplot_ax, **rp_kwargs + ) + + elif view == "pull": + pulls = (hist_values - compare_values) / hist_values_uncert - plt.xlim(left_edge, right_edge) + pulls[np.isnan(pulls) | np.isinf(pulls)] = 0 + + # Pass dicts instead of unpacking to avoid conflicts + subplot_ax = plot_pull( + self, pulls, ax=subplot_ax, bar_kwargs=bar_kwargs, pp_kwargs=pp_kwargs + ) - pull_ax.set_xlabel(self.axes[0].label) - pull_ax.set_ylabel("Pull") + if main_ax.get_legend_handles_labels()[0]: # Don't plot an empty legend + main_ax.legend(loc=rp_kwargs["legend_loc"]) - return main_ax, pull_ax + return main_ax, subplot_ax def get_center(x: Union[str, int, Tuple[float, float]]) -> Union[str, float]: From 2a37f9d9beff3a22ae74f31fca05db0fc0c70d35 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Thu, 8 Apr 2021 21:59:19 -0500 Subject: [PATCH 2/8] Add test images for plot_ratio --- tests/baseline/test_image_plot_pull.png | Bin 21845 -> 20789 bytes .../test_image_plot_ratio_callable.png | Bin 0 -> 19244 bytes tests/baseline/test_image_plot_ratio_hist.png | Bin 0 -> 13754 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/baseline/test_image_plot_ratio_callable.png create mode 100644 tests/baseline/test_image_plot_ratio_hist.png diff --git a/tests/baseline/test_image_plot_pull.png b/tests/baseline/test_image_plot_pull.png index 7bfde20305a757be1788db6ce0c7b0c41b5f8ed9..b535486021c5be1431284759cae3ab2b7d534aaf 100644 GIT binary patch literal 20789 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%0D8+4A@pa?is%9V+x~E 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 literal 21845 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 LPl)b}N-z5dt|9Xj literal 0 HcmV?d00001 diff --git a/tests/baseline/test_image_plot_ratio_hist.png b/tests/baseline/test_image_plot_ratio_hist.png new file mode 100644 index 0000000000000000000000000000000000000000..ddc83f2233ce15a5148265522225dbef97fb9f37 GIT binary patch literal 13754 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 R1-wUvD9WmV@5l7{{{lxiKC%D+ literal 0 HcmV?d00001 From a8d98985042520d5c957ac963f9137a14f9700a4 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Thu, 8 Apr 2021 22:00:10 -0500 Subject: [PATCH 3/8] Add plotting tests for plot_ratio --- tests/test_plot.py | 64 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/tests/test_plot.py b/tests/test_plot.py index 1b6d55c2..fa76f4d9 100644 --- a/tests/test_plot.py +++ b/tests/test_plot.py @@ -620,7 +620,69 @@ def pdf(x, a=1 / np.sqrt(2 * np.pi), x0=0, sigma=1, offset=0): fig = plt.figure() - assert h.plot_pull(pdf, fit_fmt=r"{name} = {value:.3g} $\pm$ {error:.3g}") + assert h.plot_pull( + pdf, + eb_color="black", + fp_color="blue", + ub_color="lightblue", + fit_fmt=r"{name} = {value:.3g} $\pm$ {error:.3g}", + ) + + return fig + + +@pytest.mark.mpl_image_compare(baseline_dir="baseline", savefig_kwargs={"dpi": 50}) +def test_image_plot_ratio_hist(): + """ + Test plot_pull by comparing against a reference image generated via + `pytest --mpl-generate-path=tests/baseline` + """ + + np.random.seed(42) + + hist_1 = Hist( + axis.Regular( + 50, -5, 5, name="X", label="x [units]", underflow=False, overflow=False + ) + ).fill(np.random.normal(size=1000)) + hist_2 = Hist( + axis.Regular( + 50, -5, 5, name="X", label="x [units]", underflow=False, overflow=False + ) + ).fill(np.random.normal(size=1700)) + + fig = plt.figure() + + assert hist_1.plot_ratio( + hist_2, rp_num_label="numerator", rp_denom_label="denominator" + ) + + return fig + + +@pytest.mark.mpl_image_compare(baseline_dir="baseline", savefig_kwargs={"dpi": 50}) +def test_image_plot_ratio_callable(): + """ + Test plot_pull by comparing against a reference image generated via + `pytest --mpl-generate-path=tests/baseline` + """ + + np.random.seed(42) + + hist_1 = Hist( + axis.Regular( + 50, -5, 5, name="X", label="x [units]", underflow=False, overflow=False + ) + ).fill(np.random.normal(size=1000)) + + def model(x, a=1 / np.sqrt(2 * np.pi), x0=0, sigma=1, offset=0): + return a * np.exp(-((x - x0) ** 2) / (2 * sigma ** 2)) + offset + + fig = plt.figure() + + assert hist_1.plot_ratio( + model, eb_color="black", fp_color="blue", ub_color="lightblue" + ) return fig From 6d034ad96ce388bfcfd8ae2d6eb481957a7dc7ab Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Thu, 8 Apr 2021 22:00:33 -0500 Subject: [PATCH 4/8] Update README and Changelog to reflect addition of plot_ratio --- README.md | 1 + docs/changelog.rst | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/README.md b/README.md index 31d487d5..89fe4090 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Hist currently provides everything boost-histogram provides, and the following e - Quick plotting routines encourage exploration: - `.plot()` provides 1D and 2D plots - `.plot2d_full()` shows 1D projects around a 2D plot + - `.plot_ratio(...)` make a ratio plot between the histogram and another histogram or callable - `.plot_pull(...)` performs a pull plot - `.plot_pie()` makes a pie plot - `.show()` provides a nice str printout using Histoprint diff --git a/docs/changelog.rst b/docs/changelog.rst index c42f2204..a8ba2a3a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,10 @@ Changelog IN PROGRESS -------------------- +* Add ``plot_ratio`` to the public API, which allows for making ratio plots between the + histogram and either another histogram or a callable. + `#161 `_ + * Add frequentist coverage interval support in the ``intervals`` module. `#176 `_ From ee8e8e65b127f9431be3fee3efc6bc3a1fec32d1 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Thu, 8 Apr 2021 22:04:30 -0500 Subject: [PATCH 5/8] Add NameTuples for typing * force return type to be np.ndarray * type before to deal with control flow --- src/hist/basehist.py | 16 +++++-- src/hist/plot.py | 106 ++++++++++++++++++++++++++++++++----------- 2 files changed, 92 insertions(+), 30 deletions(-) diff --git a/src/hist/basehist.py b/src/hist/basehist.py index ca1c09ec..45c1665b 100644 --- a/src/hist/basehist.py +++ b/src/hist/basehist.py @@ -35,6 +35,8 @@ import matplotlib.axes from mplhep.plot import Hist1DArtists, Hist2DArtists + from .plot import FitResultArtists, MainAxisArtists, RatiolikeArtists + InnerIndexing = Union[ SupportsIndex, str, Callable[[bh.axis.Axis], int], slice, "ellipsis" ] @@ -407,9 +409,12 @@ def plot_ratio( *, ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, **kwargs: Any, - ) -> "Tuple[matplotlib.axes.Axes, matplotlib.axes.Axes]": + ) -> "Tuple[MainAxisArtists, RatiolikeArtists]": """ - plot_ratio method for BaseHist object. + ``plot_ratio`` method for ``BaseHist`` object. + + Return a tuple of artists following a structure of + ``(main_ax_artists, subplot_ax_artists)`` """ import hist.plot @@ -424,9 +429,12 @@ def plot_pull( *, ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, **kwargs: Any, - ) -> "Tuple[matplotlib.axes.Axes, matplotlib.axes.Axes]": + ) -> "Tuple[FitResultArtists, RatiolikeArtists]": """ - Plot_pull method for BaseHist object. + ``plot_pull`` method for ``BaseHist`` object. + + Return a tuple of artists following a structure of + ``(main_ax_artists, subplot_ax_artists)`` """ import hist.plot diff --git a/src/hist/plot.py b/src/hist/plot.py index 62c86470..47561f8a 100644 --- a/src/hist/plot.py +++ b/src/hist/plot.py @@ -1,6 +1,17 @@ import inspect import sys -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + NamedTuple, + Optional, + Set, + Tuple, + Union, +) import numpy as np @@ -23,6 +34,34 @@ raise +class FitResultArtists(NamedTuple): + line: matplotlib.lines.Line2D + errorbar: matplotlib.container.ErrorbarContainer + band: matplotlib.collections.PolyCollection + + +class RatioErrorbarArtists(NamedTuple): + line: matplotlib.lines.Line2D + errorbar: matplotlib.container.ErrorbarContainer + + +class RatioBarArtists(NamedTuple): + line: matplotlib.lines.Line2D + dots: matplotlib.collections.PathCollection + bar: matplotlib.container.BarContainer + + +class PullArtists(NamedTuple): + bar: matplotlib.container.BarContainer + patch_artist: List[matplotlib.patches.Rectangle] + + +MainAxisArtists = Union[FitResultArtists, Hist1DArtists] + +RatioArtists = Union[RatioErrorbarArtists, RatioBarArtists] +RatiolikeArtists = Union[RatioArtists, PullArtists] + + __all__ = ( "histplot", "hist2dplot", @@ -235,14 +274,18 @@ def _construct_gaussian_callable( mean = (hist_values * x_values).sum() / hist_values.sum() sigma = (hist_values * np.square(x_values - mean)).sum() / hist_values.sum() + # gauss is a closure that will get evaluated in _fit_callable_to_hist def gauss( x: np.ndarray, constant: float = constant, mean: float = mean, sigma: float = sigma, - ) -> Any: - # Note: As return is a numpy ufuncs the type is "Any" - return constant * np.exp(-np.square(x - mean) / (2 * np.square(sigma))) + ) -> np.ndarray: + # Note: Force np.ndarray type as numpy ufuncs have type "Any" + ret: np.ndarray = constant * np.exp( + -np.square(x - mean) / (2 * np.square(sigma)) + ) + return ret return gauss @@ -288,7 +331,7 @@ def _plot_fit_result( eb_kwargs: Dict[str, Any], fp_kwargs: Dict[str, Any], ub_kwargs: Dict[str, Any], -) -> List[matplotlib.artist.Artist]: +) -> FitResultArtists: """ Plot fit of model to histogram data """ @@ -300,23 +343,23 @@ def _plot_fit_result( ) hist_uncert = np.sqrt(variances) - _errorbars = ax.errorbar(x_values, _hist.values(), hist_uncert, **eb_kwargs) + errorbars = ax.errorbar(x_values, _hist.values(), hist_uncert, **eb_kwargs) # Ensure zorder draws data points above model - line_zorder = _errorbars[0].get_zorder() - 1 + line_zorder = errorbars[0].get_zorder() - 1 (line,) = ax.plot(x_values, model_values, **fp_kwargs, zorder=line_zorder) # Uncertainty band for fitted function # TODO: Probably set a better default color than the fit line color ub_kwargs.setdefault("color", line.get_color()) - ax.fill_between( + uncertainty_band = ax.fill_between( x_values, model_values - model_uncert, model_values + model_uncert, **ub_kwargs, ) - return ax.get_children() + return FitResultArtists(line, errorbars, uncertainty_band) def plot_ratio( @@ -325,7 +368,7 @@ def plot_ratio( ratio_uncert: np.ndarray, ax: matplotlib.axes.Axes, **kwargs: Any, -) -> matplotlib.axes.Axes: +) -> RatioArtists: """ Plot a ratio plot on the given axes """ @@ -338,11 +381,16 @@ def plot_ratio( ratio[np.isinf(ratio)] = np.nan central_value = kwargs.pop("central_value", 1.0) - ax.axhline(central_value, color="black", linestyle="dashed", linewidth=1.0) + central_value_artist = ax.axhline( + central_value, color="black", linestyle="dashed", linewidth=1.0 + ) + + # Type now due to control flow + axis_artists: Union[RatioErrorbarArtists, RatioBarArtists] uncert_draw_type = kwargs.pop("uncert_draw_type", "line") if uncert_draw_type == "line": - ax.errorbar( + errorbar_artists = ax.errorbar( x_values, ratio, yerr=ratio_uncert, @@ -350,6 +398,7 @@ def plot_ratio( marker="o", linestyle="none", ) + axis_artists = RatioErrorbarArtists(central_value_artist, errorbar_artists) elif uncert_draw_type == "bar": bar_width = (right_edge - left_edge) / len(ratio) @@ -363,7 +412,7 @@ def plot_ratio( # Ensure zorder draws data points above uncertainty bars bar_zorder = _ratio_points.get_zorder() - 1 - ax.bar( + bar_artists = ax.bar( x_values, height=bar_height, width=bar_width, @@ -374,6 +423,7 @@ def plot_ratio( hatch=3 * "/", zorder=bar_zorder, ) + axis_artists = RatioBarArtists(central_value_artist, _ratio_points, bar_artists) ratio_ylim = kwargs.pop("ylim", None) if ratio_ylim is None: @@ -401,7 +451,7 @@ def plot_ratio( ax.set_xlabel(_hist.axes[0].label) ax.set_ylabel(kwargs.pop("ylabel", "Ratio")) - return ax + return axis_artists def plot_pull( @@ -410,7 +460,7 @@ def plot_pull( ax: matplotlib.axes.Axes, bar_kwargs: Dict[str, Any], pp_kwargs: Dict[str, Any], -) -> matplotlib.axes.Axes: +) -> PullArtists: """ Plot a pull plot on the given axes """ @@ -420,11 +470,12 @@ def plot_pull( # Pull: plot the pulls using Matplotlib bar method width = (right_edge - left_edge) / len(pulls) - ax.bar(x_values, pulls, width=width, **bar_kwargs) + bar_artists = ax.bar(x_values, pulls, width=width, **bar_kwargs) pp_num = pp_kwargs.pop("num", 5) patch_height = max(np.abs(pulls)) / pp_num patch_width = width * len(pulls) + patch_artists = [] for i in range(pp_num): # gradient color patches if "alpha" in pp_kwargs: @@ -442,13 +493,14 @@ def plot_pull( downRect_startpoint, patch_width, patch_height, **pp_kwargs ) ax.add_patch(downRect) + patch_artists.append((downRect, upRect)) ax.set_xlim(left_edge, right_edge) ax.set_xlabel(_hist.axes[0].label) ax.set_ylabel("Pull") - return ax + return PullArtists(bar_artists, patch_artists) def _plot_ratiolike( @@ -460,7 +512,7 @@ def _plot_ratiolike( view: Literal["ratio", "pull"], fit_fmt: Optional[str] = None, **kwargs: Any, -) -> "Tuple[matplotlib.axes.Axes, matplotlib.axes.Axes]": +) -> Tuple[MainAxisArtists, RatiolikeArtists]: r""" Plot ratio-like plots (ratio plots and pull plots) for BaseHist @@ -538,6 +590,7 @@ def _plot_ratiolike( # Computation and Fit hist_values = self.values() + main_ax_artists: MainAxisArtists # Type now due to control flow if callable(other) or isinstance(other, str): if isinstance(other, str): if other in {"gauss", "gaus", "normal"}: @@ -565,9 +618,7 @@ def _plot_ratiolike( else: fp_kwargs["label"] = "Fitted value" - # TODO FIXME - # main_ax = _plot_fit_result( - _plot_fit_result( + main_ax_artists = _plot_fit_result( self, model_values=compare_values, model_uncert=model_uncert, @@ -579,9 +630,12 @@ def _plot_ratiolike( else: compare_values = other.values() - histplot(self, ax=main_ax, label=rp_kwargs["num_label"]) - histplot(other, ax=main_ax, label=rp_kwargs["denom_label"]) + self_artists = histplot(self, ax=main_ax, label=rp_kwargs["num_label"]) + other_artists = histplot(other, ax=main_ax, label=rp_kwargs["denom_label"]) + + main_ax_artists = self_artists, other_artists + subplot_ax_artists: RatiolikeArtists # Type now due to control flow # Compute ratios: containing no INF values with np.errstate(divide="ignore", invalid="ignore"): if view == "ratio": @@ -592,7 +646,7 @@ def _plot_ratiolike( uncertainty_type=rp_kwargs["uncertainty_type"], ) # ratio: plot the ratios using Matplotlib errorbar or bar - subplot_ax = plot_ratio( + subplot_ax_artists = plot_ratio( self, ratios, ratio_uncert, ax=subplot_ax, **rp_kwargs ) @@ -602,14 +656,14 @@ def _plot_ratiolike( pulls[np.isnan(pulls) | np.isinf(pulls)] = 0 # Pass dicts instead of unpacking to avoid conflicts - subplot_ax = plot_pull( + subplot_ax_artists = plot_pull( self, pulls, ax=subplot_ax, bar_kwargs=bar_kwargs, pp_kwargs=pp_kwargs ) if main_ax.get_legend_handles_labels()[0]: # Don't plot an empty legend main_ax.legend(loc=rp_kwargs["legend_loc"]) - return main_ax, subplot_ax + return main_ax_artists, subplot_ax_artists def get_center(x: Union[str, int, Tuple[float, float]]) -> Union[str, float]: From b9d0942f30e325d62a5048c7b13d2fe397a925c8 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Sat, 10 Apr 2021 00:38:02 -0500 Subject: [PATCH 6/8] Fixups thanks to Henry's advice * Ensure default uncertainty band color visible * bump location of __all__ to just below imports * Remove quotes * Rename _hist vars to either __hist or histogram * Add test for str alias for plot_ratio and plot_pull * Add str to types for plot_ratiolike * use TypeVar to type hist.BaseHist --- src/hist/basehist.py | 4 +-- src/hist/plot.py | 76 ++++++++++++++++++++++---------------------- tests/test_plot.py | 23 +++++++++++--- 3 files changed, 59 insertions(+), 44 deletions(-) diff --git a/src/hist/basehist.py b/src/hist/basehist.py index 45c1665b..cb4758cf 100644 --- a/src/hist/basehist.py +++ b/src/hist/basehist.py @@ -405,7 +405,7 @@ def plot2d_full( def plot_ratio( self, - other: Callable[[np.ndarray], np.ndarray], + other: Union[T, Callable[[np.ndarray], np.ndarray], str], *, ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, **kwargs: Any, @@ -425,7 +425,7 @@ def plot_ratio( def plot_pull( self, - func: Callable[[np.ndarray], np.ndarray], + func: Union[Callable[[np.ndarray], np.ndarray], str], *, ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, **kwargs: Any, diff --git a/src/hist/plot.py b/src/hist/plot.py index 47561f8a..b6c975bf 100644 --- a/src/hist/plot.py +++ b/src/hist/plot.py @@ -33,6 +33,15 @@ ) raise +__all__ = ( + "histplot", + "hist2dplot", + "plot2d_full", + "plot_ratio", + "plot_pull", + "plot_pie", +) + class FitResultArtists(NamedTuple): line: matplotlib.lines.Line2D @@ -62,16 +71,6 @@ class PullArtists(NamedTuple): RatiolikeArtists = Union[RatioArtists, PullArtists] -__all__ = ( - "histplot", - "hist2dplot", - "plot2d_full", - "plot_ratio", - "plot_pull", - "plot_pie", -) - - def __dir__() -> Tuple[str, ...]: return __all__ @@ -183,9 +182,9 @@ def fnll(v: Iterable[np.ndarray]) -> float: def plot2d_full( self: hist.BaseHist, *, - ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, + ax_dict: Optional[Dict[str, matplotlib.axes.Axes]] = None, **kwargs: Any, -) -> "Tuple[Hist2DArtists, Hist1DArtists, Hist1DArtists]": +) -> Tuple[Hist2DArtists, Hist1DArtists, Hist1DArtists]: """ Plot2d_full method for BaseHist object. @@ -264,10 +263,10 @@ def plot2d_full( def _construct_gaussian_callable( - _hist: hist.BaseHist, -) -> Callable[[np.ndarray, float, float, float], np.ndarray]: - x_values = _hist.axes[0].centers - hist_values = _hist.values() + __hist: hist.BaseHist, +) -> Callable[[np.ndarray], np.ndarray]: + x_values = __hist.axes[0].centers + hist_values = __hist.values() # gaussian with reasonable initial guesses for parameters constant = float(hist_values.max()) @@ -292,13 +291,13 @@ def gauss( def _fit_callable_to_hist( model: Callable[[np.ndarray], np.ndarray], - _hist: hist.BaseHist, + histogram: hist.BaseHist, likelihood: bool = False, ) -> "Tuple[np.ndarray, np.ndarray, np.ndarray, Tuple[Tuple[float, ...], np.ndarray]]": """ Fit a model, a callable function, to the histogram values. """ - variances = _hist.variances() + variances = histogram.variances() if variances is None: raise RuntimeError( "Cannot compute from a variance-less histogram, try a Weight storage" @@ -306,9 +305,9 @@ def _fit_callable_to_hist( hist_uncert = np.sqrt(variances) # Infer best fit model parameters and covariance matrix - xdata = _hist.axes[0].centers + xdata = histogram.axes[0].centers popt, pcov = _curve_fit_wrapper( - model, xdata, _hist.values(), hist_uncert, likelihood=likelihood + model, xdata, histogram.values(), hist_uncert, likelihood=likelihood ) model_values = model(xdata, *popt) @@ -324,7 +323,7 @@ def _fit_callable_to_hist( def _plot_fit_result( - _hist: hist.BaseHist, + __hist: hist.BaseHist, model_values: np.ndarray, model_uncert: np.ndarray, ax: matplotlib.axes.Axes, @@ -335,23 +334,24 @@ def _plot_fit_result( """ Plot fit of model to histogram data """ - x_values = _hist.axes[0].centers - variances = _hist.variances() + x_values = __hist.axes[0].centers + variances = __hist.variances() if variances is None: raise RuntimeError( "Cannot compute from a variance-less histogram, try a Weight storage" ) hist_uncert = np.sqrt(variances) - errorbars = ax.errorbar(x_values, _hist.values(), hist_uncert, **eb_kwargs) + errorbars = ax.errorbar(x_values, __hist.values(), hist_uncert, **eb_kwargs) # Ensure zorder draws data points above model line_zorder = errorbars[0].get_zorder() - 1 (line,) = ax.plot(x_values, model_values, **fp_kwargs, zorder=line_zorder) # Uncertainty band for fitted function - # TODO: Probably set a better default color than the fit line color ub_kwargs.setdefault("color", line.get_color()) + if ub_kwargs["color"] == line.get_color(): + ub_kwargs.setdefault("alpha", 0.3) uncertainty_band = ax.fill_between( x_values, model_values - model_uncert, @@ -363,7 +363,7 @@ def _plot_fit_result( def plot_ratio( - _hist: hist.BaseHist, + __hist: hist.BaseHist, ratio: np.ndarray, ratio_uncert: np.ndarray, ax: matplotlib.axes.Axes, @@ -372,9 +372,9 @@ def plot_ratio( """ Plot a ratio plot on the given axes """ - x_values = _hist.axes[0].centers - left_edge = _hist.axes.edges[0][0] - right_edge = _hist.axes.edges[-1][-1] + x_values = __hist.axes[0].centers + left_edge = __hist.axes.edges[0][0] + right_edge = __hist.axes.edges[-1][-1] # Set 0 and inf to nan to hide during plotting ratio[ratio == 0] = np.nan @@ -448,14 +448,14 @@ def plot_ratio( ax.set_xlim(left_edge, right_edge) ax.set_ylim(bottom=ratio_ylim[0], top=ratio_ylim[1]) - ax.set_xlabel(_hist.axes[0].label) + ax.set_xlabel(__hist.axes[0].label) ax.set_ylabel(kwargs.pop("ylabel", "Ratio")) return axis_artists def plot_pull( - _hist: hist.BaseHist, + __hist: hist.BaseHist, pulls: np.ndarray, ax: matplotlib.axes.Axes, bar_kwargs: Dict[str, Any], @@ -464,9 +464,9 @@ def plot_pull( """ Plot a pull plot on the given axes """ - x_values = _hist.axes[0].centers - left_edge = _hist.axes.edges[0][0] - right_edge = _hist.axes.edges[-1][-1] + x_values = __hist.axes[0].centers + left_edge = __hist.axes.edges[0][0] + right_edge = __hist.axes.edges[-1][-1] # Pull: plot the pulls using Matplotlib bar method width = (right_edge - left_edge) / len(pulls) @@ -497,7 +497,7 @@ def plot_pull( ax.set_xlim(left_edge, right_edge) - ax.set_xlabel(_hist.axes[0].label) + ax.set_xlabel(__hist.axes[0].label) ax.set_ylabel("Pull") return PullArtists(bar_artists, patch_artists) @@ -505,10 +505,10 @@ def plot_pull( def _plot_ratiolike( self: hist.BaseHist, - other: Union[hist.BaseHist, Callable[[np.ndarray], np.ndarray]], + other: Union[hist.BaseHist, Callable[[np.ndarray], np.ndarray], str], likelihood: bool = False, *, - ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, + ax_dict: Optional[Dict[str, matplotlib.axes.Axes]] = None, view: Literal["ratio", "pull"], fit_fmt: Optional[str] = None, **kwargs: Any, @@ -676,7 +676,7 @@ def get_center(x: Union[str, int, Tuple[float, float]]) -> Union[str, float]: def plot_pie( self: hist.BaseHist, *, - ax: "Optional[matplotlib.axes.Axes]" = None, + ax: Optional[matplotlib.axes.Axes] = None, **kwargs: Any, ) -> Any: diff --git a/tests/test_plot.py b/tests/test_plot.py index fa76f4d9..5a4d3563 100644 --- a/tests/test_plot.py +++ b/tests/test_plot.py @@ -534,10 +534,6 @@ def pdf(x, a=1 / np.sqrt(2 * np.pi), x0=0, sigma=1, offset=0): assert h.plot_pull(pdf_str) - assert h.plot_pull("gauss") - - assert h.plot_pull("gauss", likelihood=True) - # dimension error hh = NamedHist( axis.Regular( @@ -600,6 +596,25 @@ def pdf(x, a=1 / np.sqrt(2 * np.pi), x0=0, sigma=1, offset=0): plt.close("all") +@pytest.mark.parametrize("str_alias", ["normal", "gauss", "gaus"]) +@pytest.mark.parametrize("use_likelihood", [True, False]) +def test_ratiolike_str_alias(str_alias, use_likelihood): + """ + Test str alias for callable in plot_ratio and plot_pull + """ + + np.random.seed(42) + + h = NamedHist( + axis.Regular( + 50, -4, 4, name="S", label="s [units]", underflow=False, overflow=False + ) + ).fill(S=np.random.normal(size=10)) + + assert h.plot_ratio(str_alias, likelihood=use_likelihood) + assert h.plot_pull(str_alias, likelihood=use_likelihood) + + @pytest.mark.mpl_image_compare(baseline_dir="baseline", savefig_kwargs={"dpi": 50}) def test_image_plot_pull(): """ From 56e503f7f31f9cdfb58b46f8cd16bed942b88340 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Sat, 10 Apr 2021 13:00:49 -0500 Subject: [PATCH 7/8] Rename plot_x -> plot_x_array for clarity --- src/hist/plot.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/hist/plot.py b/src/hist/plot.py index b6c975bf..d69d1355 100644 --- a/src/hist/plot.py +++ b/src/hist/plot.py @@ -37,8 +37,8 @@ "histplot", "hist2dplot", "plot2d_full", - "plot_ratio", - "plot_pull", + "plot_ratio_array", + "plot_pull_array", "plot_pie", ) @@ -362,7 +362,7 @@ def _plot_fit_result( return FitResultArtists(line, errorbars, uncertainty_band) -def plot_ratio( +def plot_ratio_array( __hist: hist.BaseHist, ratio: np.ndarray, ratio_uncert: np.ndarray, @@ -454,7 +454,7 @@ def plot_ratio( return axis_artists -def plot_pull( +def plot_pull_array( __hist: hist.BaseHist, pulls: np.ndarray, ax: matplotlib.axes.Axes, @@ -646,7 +646,7 @@ def _plot_ratiolike( uncertainty_type=rp_kwargs["uncertainty_type"], ) # ratio: plot the ratios using Matplotlib errorbar or bar - subplot_ax_artists = plot_ratio( + subplot_ax_artists = plot_ratio_array( self, ratios, ratio_uncert, ax=subplot_ax, **rp_kwargs ) @@ -656,7 +656,7 @@ def _plot_ratiolike( pulls[np.isnan(pulls) | np.isinf(pulls)] = 0 # Pass dicts instead of unpacking to avoid conflicts - subplot_ax_artists = plot_pull( + subplot_ax_artists = plot_pull_array( self, pulls, ax=subplot_ax, bar_kwargs=bar_kwargs, pp_kwargs=pp_kwargs ) From 7148edfc5b5d7426b4d0f46bc1ebe2d232e8fea5 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Sat, 10 Apr 2021 15:57:04 -0500 Subject: [PATCH 8/8] Use quoted types over TypeVar Co-authored-by: Henry Schreiner --- src/hist/basehist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hist/basehist.py b/src/hist/basehist.py index cb4758cf..cf8d0511 100644 --- a/src/hist/basehist.py +++ b/src/hist/basehist.py @@ -405,7 +405,7 @@ def plot2d_full( def plot_ratio( self, - other: Union[T, Callable[[np.ndarray], np.ndarray], str], + other: Union["hist.BaseHist", Callable[[np.ndarray], np.ndarray], str], *, ax_dict: "Optional[Dict[str, matplotlib.axes.Axes]]" = None, **kwargs: Any,