Source code for atompy._histogram1d

import warnings
from os import PathLike
from typing import Any, Iterator, Literal, Self, TypedDict

import matplotlib.pyplot as plt
import numpy as np
import uproot
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from numpy.typing import ArrayLike, NDArray

from ._core import (
    deprecated_keyword_doing_nothing_msg,
    get_topmost_figure,
    raise_unmatching_edges,
)
from ._utils import centers_to_edges, get_all_dividers


class Hist1dLabelsDict(TypedDict, total=True):
    title: str
    xlabel: str
    ylabel: str


[docs] class Hist1d: """ A histogram class providing basic histogram methods. .. tip:: Histogram your data using :func:`numpy.histogram`, then wrap the results in :class:`.Hist1d`:: hist = ap.Hist1d(*np.histogram(data)) Parameters ---------- values : array_like The histogram values, e.g., counts. edges : array_like The edges of the histogram bins. Note that ``len(values) = len(edges) + 1`` .. note:: If you want to initialize a :class:`.Hist1d` from centers instead of edges, use :meth:`.Hist1d.from_centers`. title : str, default "" Optional title of the histogram. xlabel : str, default "" Optional x-label of the histogram. ylabel : str, default "" Optional y-label of the histogram. Attributes ---------- edges : ndarray bin_edges : ndarray values : ndarray hist : ndarray centers : ndarray limits : (float, float) nbins : int labels_dict : dict """ def __init__( self, values: ArrayLike, edges: ArrayLike, title: str = "", xlabel: str = "", ylabel: str = "", ): self._values = np.asarray(values).astype(np.float64) self._edges = np.asarray(edges).astype(np.float64) if len(self._values) != len(self._edges) - 1: raise ValueError("shape of values does not match shape of edges") self._centers = self._calculate_centers(self._edges) self.title = title self.xlabel = xlabel self.ylabel = ylabel
[docs] @classmethod def from_centers( cls, values: ArrayLike, centers: ArrayLike, lower: None | float = None, upper: None | float = None, title: str = "", xlabel: str = "", ylabel: str = "", ) -> Self: """ Initiate a :class:`.Hist1d` instance from values and bin-centers. If the bins don't have constant size, at least one limit has to be provided, from which the edges can be determined .. attention:: If `centers` are not the centers of *all* bins, or if `lower` or `upper` are not indeed the lower or upper edge, `from_centers` will silently produce nonsense. Parameters ---------- centers : ndarray, shape(n) centers of the bins lower, uppper : float, optional Lower/upper limits of the range. At least one limit must be provided if bins don't have a constant size. If both lower and upper limits are provided, the lower one will be prioritized. title : str, default "" Optional title of the histogram. xlabel : str, default "" Optional x-label of the histogram. ylabel : str, default "" Optional y-label of the histogram. See also -------- centers_to_edges Examples -------- Initiate a histogram with constant bin sizes:: >>> x = 0.5, 1.5, 2.5, 3.5, 4.5 >>> y = 1, 2, 3, 4, 5 >>> hist = ap.Hist1d.from_centers(x, y) >>> hist.edges [0. 1. 2. 3. 4. 5.] Initiate a histogram with non-constant bin sizes. Then, a lower (or upper) bound has to be passed:: >>> x = 0.5, 1.5, 3.0, 4.5, 5.5 >>> y = 1, 2, 3, 4, 5 >>> hist = ap.Hist1d.from_centers(x, y, lower=0.0) >>> hist.edges [0. 1. 2. 4. 5. 6.] """ edges = centers_to_edges(centers, lower, upper) values = np.asarray(values, copy=True).astype(np.float64) if len(values) != len(edges) - 1: raise ValueError("shape of values does not match shape of centers") return cls(values, edges, title, xlabel, ylabel)
[docs] @classmethod def from_txt( cls, fname: str | PathLike, data_layout: Literal["rows", "columns"] = "columns", idx_centers: int = 0, idx_values: int = 1, lower: None | float = None, upper: None | float = None, title: str = "", xlabel: str = "", ylabel: str = "", **loadtxt_kwargs, ) -> Self: """ Initiate a :class:`.Hist1d` from a text file. Assumes that the histogram is saved as bin-centers, bin-values. The file is loaded using :func:`numpy.loadtxt`. Parameters ---------- fname : str or PathLike The filename. data_layout : "rows" or "columns", default "columns" Specify if centers and values are saved in the text file in rows or in columns. idx_centers : int, default 0 The index that corresponds to the histogram bin centers. idx_values : int, default 1 The index that corresponds to the histogram values. lower, upper : float, optional If the histogram bins do not have equal size, at least `lower` or `upper` has to be provided in order to properly calculate the bin edges. See :func:`.centers_to_edges`. title : str, default "" Optional title of the histogram. xlabel : str, default "" Optional x-label of the histogram. ylabel : str, default "" Optional y-label of the histogram. Other parameters ---------------- **loadtxt_kwargs Additional :func:`numpy.loadtxt` keyword arguments. Examples -------- Assume a ``data.txt`` with:: # data.txt 0.5 1 1.5 2 2.5 3 3.5 4 4.5 5 Initiate a histogram from it:: >>> hist = ap.Hist1d.from_txt("data.txt") >>> hist.values array([1. 2. 3. 4. 5.]) >>> hist.edges array([0. 1. 2. 3. 4. 5]) If the bins do not have a constant binsize, e.g.:: # data.txt 0.5 1 1.5 2 3.0 3 4.5 4 5.5 5 one can load it, but needs to specify either `lower` or `upper`:: >>> hist = ap.Hist1d.from_txt("data.txt", lower=0.0) >>> hist.values array([1. 2. 3. 4. 5.]) >>> hist.edges array([0. 1. 2. 4. 5. 6]) If multiple datasets are within one textfile, e.g.:: # manydata.txt # values1 values2 centers 1 11 0.5 2 12 1.5 3 13 2.5 4 14 3.5 5 15 4.5 one can load specify which data to load using the `idx_*` keywords:: >>> hist1 = ap.Hist1d.from_txt("manydata.txt", idx_centers=2, idx_values=0) >>> hist2 = ap.Hist1d.from_txt("manydata.txt", idx_centers=2, idx_values=1) >>> hist1.values array([1. 2. 3. 4. 5.]) >>> hist2.values array([11. 12. 13. 14. 15.]) """ data = np.loadtxt(fname, **loadtxt_kwargs) if data_layout == "columns": data = data.T elif data_layout != "rows": raise ValueError(f"{data_layout=}, but it should be 'rows' or 'columns'") return cls.from_centers( data[idx_values], data[idx_centers], lower, upper, title, xlabel, ylabel )
[docs] @classmethod def from_root( cls, fname: str | PathLike, hname: str, title: str | Literal["__auto__"] = "__auto__", xlabel: str | Literal["__auto__"] = "__auto__", ylabel: str | Literal["__auto__"] = "__auto__", ) -> Self: """ Initiate a :class:`.Hist1d` from a `ROOT <https://root.cern.ch/>`__ file. Parameters ---------- fname : str or PathLike The filename of the ROOT file, e.g., ``important_data.root`` hname : str The name of the histogram within the ROOT file, e.g., ``path/to/histogram1d``. """ with uproot.open(fname) as file: # type: ignore hist: Any = file[hname] title_: str = hist.title if title == "__auto__" else title xlabel_: str = ( hist.member("fXaxis").member("fTitle") if xlabel == "__auto__" else xlabel ) ylabel_: str = ( hist.member("fYaxis").member("fTitle") if ylabel == "__auto__" else ylabel ) values, edges = hist.to_numpy() return cls(values, edges, title_, xlabel_, ylabel_)
@staticmethod def _calculate_centers(edges: NDArray[np.float64]) -> NDArray[np.float64]: return edges[:-1] + 0.5 * np.diff(edges).astype(np.float64) @property def values(self) -> NDArray[np.float64]: """Histogram's values (e.g., counts).""" return self._values @values.setter def values(self, values: ArrayLike) -> None: values = np.asarray(values, copy=True).astype(np.float64) if len(values) != len(self.values): raise ValueError("shape of new values does not match shape of old values") self._values = values @property def hist(self) -> NDArray[np.float64]: """Alias for :attr:`.Hist1d.values`""" return self.values @hist.setter def hist(self, new_hist: ArrayLike) -> None: self.values = new_hist @property def bin_edges(self) -> NDArray[np.float64]: """Alias for :attr:`.Hist1d.edges`""" return self.edges @bin_edges.setter def bin_edges(self, new_edges: ArrayLike) -> None: self.edges = new_edges @property def edges(self) -> NDArray[np.float64]: """Edges of the histogram's bins.""" return self._edges @edges.setter def edges(self, edges: ArrayLike) -> None: edges = np.asarray(edges, copy=True).astype(np.float64) if len(edges) != len(self.edges): raise ValueError("shape of new edges does not match shape of old edges") self._edges = edges self._centers = self._calculate_centers(self._edges) @property def centers(self) -> NDArray[np.float64]: """Centers of the histogram's bins.""" return self._centers @property def nbins(self) -> int: """Number of bins.""" return len(self.values) @property def limits(self) -> tuple[float, float]: """Limits of the histogram's edges.""" return float(self.edges[0]), float(self.edges[-1]) @property def labels_dict(self) -> Hist1dLabelsDict: """A dictionary of the histogram's title, xlabel and ylabel.""" return {"title": self.title, "xlabel": self.xlabel, "ylabel": self.ylabel} def __add__(self, other: "Hist1d") -> Self: if not isinstance(other, Hist1d): return NotImplemented raise_unmatching_edges(self.edges, other.edges) return type(self)( self.values + other.values, self.edges.copy(), **self.labels_dict ) def __iadd__(self, other: "Hist1d") -> Self: if not isinstance(other, Hist1d): return NotImplemented self.title = f"{self.title} + {other.title}" self.values += other.values return self def __sub__(self, other: "Hist1d") -> Self: if not isinstance(other, Hist1d): return NotImplemented raise_unmatching_edges(self.edges, other.edges) return type(self)( self.values - other.values, self.edges.copy(), **self.labels_dict ) def __isub__(self, other: "Hist1d") -> Self: if not isinstance(other, Hist1d): return NotImplemented self.values -= other.values self.title = f"{self.title} $-$ {other.title}" return self def __mul__(self, other: "Hist1d") -> Self: if not isinstance(other, Hist1d): return NotImplemented raise_unmatching_edges(self.edges, other.edges) return type(self)( self.values * other.values, self.edges.copy(), **self.labels_dict ) def __imul__(self, other: "Hist1d") -> Self: if not isinstance(other, Hist1d): return NotImplemented self.values *= other.values self.title = rf"{self.title} $\times$ {other.title}" return self def __truediv__(self, other: "Hist1d") -> Self: if not isinstance(other, Hist1d): return NotImplemented raise_unmatching_edges(self.edges, other.edges) return type(self)( self.values / other.values, self.edges.copy(), **self.labels_dict ) def __itruediv__(self, other: "Hist1d") -> Self: if not isinstance(other, Hist1d): return NotImplemented self.values /= other.values self.title = f"{self.title} / {other.title}" return self def __floordiv__(self, other: "Hist1d") -> Self: if not isinstance(other, Hist1d): return NotImplemented raise_unmatching_edges(self.edges, other.edges) return type(self)( self.values // other.values, self.edges.copy(), **self.labels_dict ) def __ifloordiv__(self, other: "Hist1d") -> Self: if not isinstance(other, Hist1d): return NotImplemented self.values //= other.values self.title = rf"$\lfloor${self.title} / {other.title}$\rfloor$" return self def __neg__(self) -> Self: return type(self)(-self.values, self.edges, **self.labels_dict) def __iter__(self) -> Iterator[NDArray[Any]]: return iter([self.values, self.centers]) def __str__(self) -> str: edges_str = str(self.edges) values_str = str(self.values) hist_str = f"Hist1d with (values, xedges, yedges) =\n{values_str}\n{edges_str}" return hist_str
[docs] def convert_cosine_to_angles(self, full_range: bool = True) -> Self: """ Convert edges which represent `cosine(angle)` to `angle`. Edges must not exceed the interval [-1, 1]. Parameters ---------- full_range : bool, default True Control the output range. - If False, return a histogram ranging from 0 to π. - If True, return a histogram ranging from 0 to 2π, where the second half is a mirror image of the first. Raises ------ ValueError Raised if the edges do not represent cosine values. Returns ------- hist : :class:`.Hist1d` See also -------- convert_cosine_to_angles """ cosines = np.flip(self.edges) values = np.flip(self.values) angles = np.arccos(cosines) if full_range: angles = np.append(angles, angles[1:] + np.pi) values = np.append(values, np.flip(values)) return type(self)(values, angles, **self.labels_dict)
[docs] def rebin(self, factor: int) -> Self: """ Rebin histogram. Parameters ---------- factor : int This is how many old bins will be combined to a new bin. Number of old bins divided by factor must be an integer. .. note:: Use :func:`.get_all_dividers` to find all possible rebin factors. Returns ------- new_histogram : :class:`.Hist1d` The new, rebinned histogram Examples -------- .. plot:: _examples/histogram1d/rebin.py :include-source: """ old_n = self.nbins if old_n % factor != 0: raise ValueError( f"Invalid {factor=}. Possible factors for this histogram are {get_all_dividers(old_n)}." ) new_hist = np.empty(self.values.size // factor) for i in range(new_hist.size): new_hist[i] = np.sum(self.values[i * factor : i * factor + factor]) new_edges = np.full(new_hist.size + 1, self.edges[-1]) for i in range(new_edges.size - 1): new_edges[i] = self.edges[i * factor] return type(self)(new_hist, new_edges, **self.labels_dict)
[docs] def binsizes(self) -> NDArray[np.float64]: """ Return the widths of all bins. Returns ------- binsizes : ndarray Examples -------- :: >>> ap.Hist1d((1, 2, 3), (0, 1, 2, 3).binsizes() [1. 1. 1.] >>> ap.Hist1d((1, 2, 3), (0, 1, 3, 4).binsizes() [1. 2. 1.] """ return np.diff(self.edges)
[docs] def integrate(self) -> float: """ Return the integral of the histogram. The integral is calculated as bin-value * bin-width. Returns ------- integral : float See also -------- max min sum """ return np.sum(self.values * self.binsizes())
[docs] def sum(self) -> float: """ Return the sum of the histogram's values. Returns ------- sum : float See also -------- integrate max min """ return np.sum(self.values)
[docs] def max(self) -> float: """ Return the maximum value of the histogram. Returns ------- max : float See also -------- integrate min sum """ return np.amax(self.values)
[docs] def min(self) -> float: """ Return the minimum value of the histogram. Returns ------- min : float See also -------- integrate max sum """ return np.amin(self.values)
[docs] def norm_to_integral(self) -> Self: """ Return the histogram normalized to :meth:`.Hist1d.integrate`. Returns ------- hist : :class:`Hist1d` The normalized histogram. See also -------- norm_to_max norm_to_sum """ new_values = np.divide(self.values, self.integrate()).copy() new_edges = self.edges.copy() return type(self)(new_values, new_edges, **self.labels_dict)
[docs] def norm_to_max(self) -> Self: """ Return the histogram normalized to :meth:`.Hist1d.max`. Returns ------- hist : :class:`Hist1d` The normalized histogram. See also -------- norm_to_integral norm_to_sum """ new_values = np.divide(self.values, self.values.max()).copy() new_edges = self.edges.copy() return type(self)(new_values, new_edges, **self.labels_dict)
[docs] def norm_to_sum(self) -> Self: """ Return the histogram normalized to :meth:`.Hist1d.sum`. .. note:: When comparing two histograms, :meth:`.Hist1d.norm_to_integral` may be more appropriate! Returns ------- hist : :class:`Hist1d` The normalized histogram. See also -------- norm_to_integral norm_to_max """ new_values = np.divide(self.values, self.sum()).copy() new_edges = self.edges.copy() return type(self)(new_values, new_edges, **self.labels_dict)
[docs] def for_step( self, extent_to: None | float = None ) -> tuple[NDArray[Any], NDArray[Any]]: """ Return arrays appropriate for plotting with :obj:`plt.step <matplotlib.pyplot.step>`. By default, ``plt.step`` needs the right edges of a bin and the corresponding bin value. See the *where* keyword argument. .. attention :: Don't use anything else but ``where="pre"`` (which is the default) in ``plt.step``. Otherwise the histogram will be shifted. Parameters ---------- extent_to : float, optional Extent the edges to this value (useful if the resulting plot should start at, e.g., zero). Returns ------- right_edges : ndarray Right edges, that is, ``Hist1d.edges[1:]``. values : ndarray Bin values, that is, ``Hist1d.values``. Examples -------- Plot ``hist: Hist1d``:: plt.step(*hist.for_step()) If ``where != "pre"``, the resulting histogram will be shifted!:: plt.step(*hist.for_step(), where="mid") # this will have shifted bins See also -------- for_bar for_plot """ if extent_to is not None: edges = np.append(self.edges, self.edges[-1]) values = np.concatenate([[extent_to], self.values, [extent_to]]) return edges, values else: return self.edges, np.append(self.values[0], self.values)
[docs] def for_plot(self) -> tuple[NDArray[Any], NDArray[Any]]: """ Return arrays appropriate for plotting with :obj:`plt.plot <matplotlib.pyplot.plot>`. Returns ------- centers : ndarray values : ndarray Examples -------- Plot ``hist: Hist1d``:: plt.plot(*hist.for_plot()) Convinient, if you want to chain histogram operations and then plot them. E.g., this:: plt.plot(hist.keep(lower, upper).rebin(2).centers, hist.keep(lower, upper).rebin(2).norm_to_integral().values) becomes:: plt.plot(*hist.keep(lower, upper).rebin(2).norm_to_integral().for_plot()) See also -------- for_bar for_step """ return self.centers, self.values
[docs] def for_bar(self) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]: """ Return arrays appropriate for plotting with :obj:`plt.bar <matplotlib.pyplot.bar>`. .. attention:: When using ``for_bar``, you cannot provide the ``widths`` keyword in :obj:`plt.bar <matplotlib.pyplot.bar>`! Returns ------- centers : ndarray values : ndarray binwidths : ndarray Examples -------- Plot ``hist: Hist1d``:: plt.bar(*hist.for_bar()) Note that you cannot provide the ``widths`` keyword when using this:: plt.bar(*hist.for_bar(), widths=widths) # invalid!!! If you want to provide your own binwidths, use :meth:`.Hist1d.for_plot` instead:: plt.bar(*hist.for_plot(), widths=widths) See also -------- for_plot for_step """ return self.centers, self.values, self.binsizes()
[docs] def copy(self) -> Self: """Return a copy of the histogram.""" return type(self)(self.values.copy(), self.edges.copy(), **self.labels_dict)
[docs] def plot( self, ax: Axes | None = None, fname: str | None = None, xlabel: str | Literal["__auto__"] = "__auto__", ylabel: str | Literal["__auto__"] = "__auto__", title: str | Literal["__auto__"] = "__auto__", logscale: bool = False, xlim: tuple[None | float, None | float] | None = None, ylim: tuple[None | float, None | float] | None = None, plot_fmt: str | None = None, savefig_kwargs: dict[str, Any] = {}, use_fixed_layout: None = None, fixed_layout_kwargs: None = None, make_me_nice: None = None, make_me_nice_kwargs: None = None, **plot_kwargs, ) -> tuple[Figure, Axes]: """ Plot the 1D histogram using :obj:`matplotlib.pyplot.plot`. Parameters ---------- fname : str, optional If provided, the plot will be saved to this file using :func:`matplotlib.figure.Figure.savefig`. xlabel : str, default "__auto__" Label for the x-axis. If "__auto__", use `Hist1d.xlabel`. ylabel : str, default "__auto__" Label for the y-axis. If "__auto__", use `Hist1d.ylabel`. title : str, default "__auto__" Title of the plot. If "__auto__", use `Hist1d.title`. logscale : bool, optional If True, use a logarithmic y scale. xlim : tuple[float | None, float | None], optional Limits for the x-axis. ylim : tuple[float | None, float | None], optional Limits for the y-axis. plot_fmt : str, optional format string for :obj:`matplotlib.pyplot.plot`. savefig_kwargs : dict, optional Additional keyword arguments passed to :func:`~matplotlib.figure.Figure.savefig`. Other parameters ---------------- plot_kwargs : dict, optional Additional keyword arguments passed to :obj:`matplotlib.pyplot.plot`. use_fixed_layout .. version-deprecated:: 5.5.0 Does nothing. fixed_layout_kwargs .. version-deprecated:: 5.5.0 Does nothing. make_me_nice .. version-deprecated:: 5.5.0 Does nothing. make_me_nice_kwargs .. version-deprecated:: 5.5.0 Does nothing. Returns ------- tuple of :class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes` A tuple containing the matplotlib Figure and Axes. Examples -------- .. plot:: _examples/histogram1d/plot.py :include-source: .. plot:: _examples/histogram1d/plot_in_axes.py :include-source: """ if ax is None: fig, ax = plt.subplots(1, 1) else: fig = get_topmost_figure(ax) if make_me_nice is not None: warnings.warn( deprecated_keyword_doing_nothing_msg("make_me_nice"), DeprecationWarning, stacklevel=2, ) if make_me_nice_kwargs is not None: warnings.warn( deprecated_keyword_doing_nothing_msg("make_me_nice_kwargs"), DeprecationWarning, stacklevel=2, ) if use_fixed_layout is not None: warnings.warn( deprecated_keyword_doing_nothing_msg("use_fixed_layout"), DeprecationWarning, stacklevel=2, ) if fixed_layout_kwargs is not None: warnings.warn( deprecated_keyword_doing_nothing_msg("fixed_layout_kwargs"), DeprecationWarning, stacklevel=2, ) if plot_fmt is None: ax.plot(*self.for_plot(), **plot_kwargs) else: ax.plot(*self.for_plot(), plot_fmt, **plot_kwargs) ax.set_xlabel(xlabel if xlabel != "__auto__" else self.xlabel) ax.set_ylabel(ylabel if ylabel != "__auto__" else self.ylabel) title_ = title if title != "__auto__" else self.title ax.set_title(title_) if title_ != "": fig.canvas.manager.set_window_title(title_) # type: ignore ax.set_xlim(xlim) # type: ignore ax.set_ylim(ylim) # type: ignore if logscale: ax.set_yscale("log") if fname is not None: fig.savefig(fname, **savefig_kwargs) return fig, ax
[docs] def plot_step( self, ax: Axes | None = None, fname: str | None = None, xlabel: str | Literal["__auto__"] = "__auto__", ylabel: str | Literal["__auto__"] = "__auto__", title: str | Literal["__auto__"] = "__auto__", logscale: bool = False, xlim: tuple[None | float, None | float] | None = None, ylim: tuple[None | float, None | float] | None = None, start_at: float | Literal["auto"] = 0.0, savefig_kwargs: dict[str, Any] = {}, use_fixed_layout: None = None, fixed_layout_kwargs: None = None, make_me_nice: None = None, make_me_nice_kwargs: None = None, **plot_kwargs, ) -> tuple[Figure, Axes]: """ Plot the 1D histogram using :obj:`matplotlib.pyplot.plot`. Parameters ---------- fname : str, optional If provided, the plot will be saved to this file using :meth:`matplotlib.figure.Figure.savefig`. xlabel : str, default "__auto__" Label for the x-axis. If "__auto__", use `Hist1d.xlabel`. ylabel : str, default "__auto__" Label for the y-axis. If "__auto__", use `Hist1d.ylabel`. title : str, default "__auto__" Title of the plot. If "__auto__", use `Hist1d.title`. logscale : bool, optional If True, use a logarithmic y scale. xlim : tuple[float | None, float | None], optional Limits for the x-axis. ylim : tuple[float | None, float | None], optional Limits for the y-axis. start_at : float or "auto", default 0.0 Value at which the steps will start and end. If "auto", start (end) at first (last) value of `Hist1d.values`. savefig_kwargs : dict, optional Additional keyword arguments passed to :func:`~matplotlib.figure.Figure.savefig`. Other parameters ---------------- plot_kwargs : dict, optional Additional keyword arguments passed to :obj:`matplotlib.pyplot.plot`. Should not contain the `drawstyle` keyword. use_fixed_layout .. version-deprecated:: 5.5.0 Does nothing. fixed_layout_kwargs .. version-deprecated:: 5.5.0 Does nothing. make_me_nice .. version-deprecated:: 5.5.0 Does nothing. make_me_nice_kwargs .. version-deprecated:: 5.5.0 Does nothing. Returns ------- tuple of :class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes` A tuple containing the matplotlib Figure and Axes. Examples -------- .. plot:: _examples/histogram1d/plot_step.py :include-source: """ if ax is None: fig, ax = plt.subplots(1, 1) else: fig = get_topmost_figure(ax) if make_me_nice is not None: warnings.warn( deprecated_keyword_doing_nothing_msg("make_me_nice"), DeprecationWarning, stacklevel=2, ) if make_me_nice_kwargs is not None: warnings.warn( deprecated_keyword_doing_nothing_msg("make_me_nice_kwargs"), DeprecationWarning, stacklevel=2, ) if use_fixed_layout is not None: warnings.warn( deprecated_keyword_doing_nothing_msg("use_fixed_layout"), DeprecationWarning, stacklevel=2, ) if fixed_layout_kwargs is not None: warnings.warn( deprecated_keyword_doing_nothing_msg("fixed_layout_kwargs"), DeprecationWarning, stacklevel=2, ) ax.set_xlabel(xlabel if xlabel != "__auto__" else self.xlabel) ax.set_ylabel(ylabel if ylabel != "__auto__" else self.ylabel) title_ = title if title != "__auto__" else self.title ax.set_title(title_) if title_ != "": fig.canvas.manager.set_window_title(title_) # type: ignore x = np.repeat(self.edges, 2) y = np.empty_like(x, dtype=np.float64) y[0] = start_at if start_at != "auto" else self.values[0] y[1:-1] = np.repeat(self.values, 2) y[-1] = start_at if start_at != "auto" else self.values[-1] ax.plot(x, y, "-", **plot_kwargs) ax.set_xlim(xlim) # type: ignore ax.set_ylim(ylim) # type: ignore if logscale: ax.set_yscale("log") if fname is not None: fig.savefig(fname, **savefig_kwargs) return fig, ax
[docs] def keep( self, lower: float, upper: float, squeeze: bool = False, setval: float = 0.0, ) -> Self: """ Keep every entry of the histogram in-between `lower` and `upper` Parameters ---------- lower : float Keep all data where the left :attr:`edges <Hist1d.edges>` are greater or equal to `lower`. upper : float Keep all data where the right :attr:`edges <Hist1d.edges>` are lesser or equal to `upper`. squeeze : bool, default False Controls if the resulting :class:`.Hist1d` has the same number of bins as the original histogram. setval : float, default 0.0 If `squeeze` is False, fill removed data with `setval`. Returns ------- hist : :class:`.Hist1d` See also -------- remove Examples -------- Create a histogram:: >>> hist = ap.Hist1d([1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5]) Only keep values within the interval [1, 4]:: >>> hist.keep(1, 4).values [0. 2. 3. 4. 0.] Fill removed values with a :data:`numpy.nan` >>> hist.keep(1, 4, setval=np.nan).values [nan 2. 3. 4. nan] Squeeze length to only include kept values:: >>> hist.keep(1, 4, squeeze=True).edges [1. 2. 3.] >>> hist.keep(1, 4, squeeze=True).values [2. 3] Example plot: .. plot:: _examples/histogram1d/keep.py :include-source: """ idx = np.flatnonzero( np.logical_and(lower <= self.edges[:-1], self.edges[1:] <= upper) ) if squeeze: new_values = self.values.copy()[idx] new_edges = self.edges.copy()[np.append(idx, idx[-1] + 1)] return type(self)( new_values, new_edges, self.title, self.xlabel, self.ylabel ) else: new_values = np.full(self.values.shape, setval) new_values[idx] = self.values[idx] new_edges = self.edges.copy() return type(self)(new_values, new_edges, **self.labels_dict)
[docs] def remove( self, lower: float, upper: float, setval: float = 0.0, ) -> Self: """ Remove every entry of the histogram in-between `lower` and `upper` By default, the interval is [lower, upper). Parameters ---------- lower : float Remove all data where the left :attr:`edges <Hist1d.edges>` are greater (or equal) to `lower`. upper : float Remove all data where the right :attr:`edges <Hist1d.edges>` are lesser (or equal) to `upper`. setval : float, default 0.0 Set removed bins to this value. Returns ------- hist : :class:`.Hist1d` See also -------- keep Examples -------- Create a histogram:: >>> hist = ap.Hist1d([1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5]) Only keep values in within the interval [1, 4]:: >>> hist.remove(1, 3).values [1. 0. 0. 0. 5.] Fill removed values with a :data:`numpy.nan` >>> hist.keep(1, 4, setval=np.nan).values [1. nan nan nan 5.] Example plot: .. plot:: _examples/histogram1d/remove.py :include-source: """ idx = np.flatnonzero( np.logical_and(lower <= self.edges[:-1], self.edges[1:] <= upper) ) new_hist = type(self)(self.values.copy(), self.edges.copy(), **self.labels_dict) new_hist.values[idx] = setval return new_hist
[docs] def norm_diff(self, other: "Hist1d") -> Self: """ Return the normalized difference between two histograms. Calculates (self - other) / (self + other). Parameters ---------- other : :class:`.Hist1d` The other histogram. Both histograms must have matching edges. Returns ------- norm_diff : :class:`.Hist1d` A new histgram of the normalized difference. Examples -------- .. plot:: _examples/histogram1d/norm_diff.py :include-source: """ raise_unmatching_edges(self.edges, other.edges, "x") new_edges = self.edges.copy() new_values = (self.values - other.values) / (self.values + other.values) return type(self)(new_values, new_edges, **self.labels_dict)
[docs] def pad_with(self, value: float) -> Self: """ Extent histogram left and right with `value`. A bin is inserted before and after the histogram. The bin has the value `value`. Parameters ---------- value : floaa Returns ------- :class:`.Hist1d` A new histogram with padding. Examples -------- .. plot:: _examples/histogram1d/pad_with.py :include-source: """ binwidths = self.binsizes() new_edges = np.empty(len(self.edges) + 2, dtype=np.float64) new_edges[0] = self.edges[0] - binwidths[0] new_edges[1:-1] = self.edges new_edges[-1] = self.edges[-1] + binwidths[-1] new_values = np.empty(self.nbins + 2, dtype=np.float64) new_values[0] = value new_values[1:-1] = self.values new_values[-1] = value return type(self)(new_values, new_edges, **self.labels_dict)