Source code for rootpy.plotting.hist

from __future__ import absolute_import

from array import array
from math import sqrt
from itertools import product
import operator
import numbers
try:
    from itertools import izip as zip
except ImportError: # will be 3.x series
    pass

import ROOT

from .. import asrootpy, QROOT, log; log = log[__name__]
from ..extern.six.moves import range
from ..base import NamedObject, NamelessConstructorObject
from ..decorators import snake_case_methods, cached_property
from ..context import invisible_canvas
from ..utils.extras import izip_exact
from ..extern.shortuuid import uuid
from .base import Plottable, dim
from .graph import Graph, _Graph1DBase


__all__ = [
    'Hist',
    'Hist1D',
    'Hist2D',
    'Hist3D',
    'HistStack',
    'Efficiency',
    'histogram',
]


def canonify_slice(s, n):
    """
    Convert a slice object into a canonical form
    to simplify treatment in histogram bin content
    and edge slicing.
    """
    if isinstance(s, (int, long)):
        return canonify_slice(slice(s, s + 1, None), n)
    start = s.start % n if s.start is not None else 0
    stop = s.stop % n if s.stop is not None else n
    step = s.step if s.step is not None else 1
    return slice(start, stop, step)


def bin_to_edge_slice(s, n):
    """
    Convert a bin slice into a bin edge slice.
    """
    s = canonify_slice(s, n)
    start = s.start
    stop = s.stop
    if start > stop:
        _stop = start + 1
        start = stop + 1
        stop = _stop
    start = max(start - 1, 0)
    step = abs(s.step)
    if stop <= 1 or start >= n - 1 or stop == start + 1:
        return slice(0, None, min(step, n - 2))
    s = slice(start, stop, abs(s.step))
    if len(range(*s.indices(n - 1))) < 2:
        return slice(start, stop, stop - start - 1)
    return s


class _HistViewBase(object):

    @staticmethod
    def _slice_repr(s):
        if isinstance(s, slice):
            return '[start={0}, stop={1}, step={2}]'.format(
                s.start, s.stop, s.step)
        else:
            return '{0}'.format(s)


class HistIndexView(_HistViewBase):

    def __init__(self, hist, idx):
        if idx.step is not None and abs(idx.step) != 1:
            raise ValueError(
                "rebinning using the global histogram bin "
                "indices is not supported")
        self.hist = hist
        self.idx = idx

    def __iter__(self):
        return self.hist.bins(idx=self.idx, overflow=True)

    def __repr__(self):
        return '{0}({1}, idx={2})'.format(
            self.__class__.__name__, self.hist, self._slice_repr(self.idx))


class HistView(_HistViewBase):

    def __init__(self, hist, x):
        if isinstance(x, slice) and x.step == 0:
            raise ValueError("rebin cannot be zero")
        self.hist = hist
        self.x = x

    @cached_property
    def xedges(self):
        return list(self.hist.xedges())[
            bin_to_edge_slice(self.x, self.hist.nbins(axis=0, overflow=True))]

    @property
    def points(self):
        return self.hist.bins_xyz(ix=self.x, proxy=False)

    def __iter__(self):
        return self.hist.bins_xyz(ix=self.x)

    def __repr__(self):
        return '{0}({1}, x={2})'.format(
            self.__class__.__name__, self.hist, self._slice_repr(self.x))


class Hist2DView(_HistViewBase):

    def __init__(self, hist, x, y):
        if isinstance(x, slice) and x.step == 0:
            raise ValueError("rebin along x cannot be zero")
        if isinstance(y, slice) and y.step == 0:
            raise ValueError("rebin along y cannot be zero")
        self.hist = hist
        self.x = x
        self.y = y

    @cached_property
    def xedges(self):
        return list(self.hist.xedges())[
            bin_to_edge_slice(self.x, self.hist.nbins(axis=0, overflow=True))]

    @cached_property
    def yedges(self):
        return list(self.hist.yedges())[
            bin_to_edge_slice(self.y, self.hist.nbins(axis=1, overflow=True))]

    @property
    def points(self):
        return self.hist.bins_xyz(ix=self.x, iy=self.y, proxy=False)

    def __iter__(self):
        return self.hist.bins_xyz(ix=self.x, iy=self.y)

    def __repr__(self):
        return '{0}({1}, x={2}, y={3})'.format(
            self.__class__.__name__, self.hist,
            self._slice_repr(self.x),
            self._slice_repr(self.y))


class Hist3DView(_HistViewBase):

    def __init__(self, hist, x, y, z):
        if isinstance(x, slice) and x.step == 0:
            raise ValueError("rebin along x cannot be zero")
        if isinstance(y, slice) and y.step == 0:
            raise ValueError("rebin along y cannot be zero")
        if isinstance(z, slice) and z.step == 0:
            raise ValueError("rebin along z cannot be zero")
        self.hist = hist
        self.x = x
        self.y = y
        self.z = z

    @cached_property
    def xedges(self):
        return list(self.hist.xedges())[
            bin_to_edge_slice(self.x, self.hist.nbins(axis=0, overflow=True))]

    @cached_property
    def yedges(self):
        return list(self.hist.yedges())[
            bin_to_edge_slice(self.y, self.hist.nbins(axis=1, overflow=True))]

    @cached_property
    def zedges(self):
        return list(self.hist.zedges())[
            bin_to_edge_slice(self.z, self.hist.nbins(axis=2, overflow=True))]

    @property
    def points(self):
        return self.hist.bins_xyz(ix=self.x, iy=self.y, iz=self.z, proxy=False)

    def __iter__(self):
        return self.hist.bins_xyz(ix=self.x, iy=self.y, iz=self.z)

    def __repr__(self):
        return '{0}({1}, x={2}, y={3}, z={4})'.format(
            self.__class__.__name__, self.hist,
            self._slice_repr(self.x),
            self._slice_repr(self.y),
            self._slice_repr(self.z))


class BinProxy(object):

    def __init__(self, hist, idx):
        self.hist = hist
        self.idx = idx
        self._sum_w2 = hist.GetSumw2()

    @property
    def xyz(self):
        return self.hist.xyz(self.idx)

    @cached_property
    def overflow(self):
        """
        Returns true if this BinProxy is for an overflow bin
        """
        indices = self.hist.xyz(self.idx)
        for i in range(self.hist.GetDimension()):
            if indices[i] == 0 or indices[i] == self.hist.nbins(i) + 1:
                return True
        return False

    @property
    def x(self):
        return self.hist.axis_bininfo(0, self.xyz[0])

    @property
    def y(self):
        return self.hist.axis_bininfo(1, self.xyz[1])

    @property
    def z(self):
        return self.hist.axis_bininfo(2, self.xyz[2])

    @property
    def value(self):
        return self.hist.GetBinContent(self.idx)

    @value.setter
    def value(self, v):
        return self.hist.SetBinContent(self.idx, v)

    @property
    def error(self):
        return self.hist.GetBinError(self.idx)

    @error.setter
    def error(self, e):
        return self.hist.SetBinError(self.idx, e)

    @property
    def sum_w2(self):
        return self._sum_w2.At(self.idx)

    @sum_w2.setter
    def sum_w2(self, w):
        self._sum_w2.SetAt(w, self.idx)

    @property
    def effective_entries(self):
        """
        Number of effective entries in this bin.
        The number of unweighted entries this bin would need to
        contain in order to have the same statistical power as this
        bin with possibly weighted entries, estimated by:

            (sum of weights) ** 2 / (sum of squares of weights)

        """
        sum_w2 = self.sum_w2
        if sum_w2 == 0:
            return abs(self.value)
        return (self.value ** 2) / sum_w2

    def __iadd__(self, other):
        self.value += other.value
        self.sum_w2 += other.sum_w2
        return self

    def __imul__(self, v):
        self.value *= v
        self.error *= v
        return self

    def __idiv__(self, v):
        self.value /= v
        self.error /= v
        return self

    def __ipow__(self, v):
        cur_value = self.value
        if cur_value == 0:
            return self
        new_value = cur_value ** v
        self.value = new_value
        self.error *= new_value / cur_value
        return self

    def __repr__(self):
        return '{0}({1}, {2})'.format(
            self.__class__.__name__, self.hist, self.idx)


class _HistBase(Plottable, NamedObject):

    TYPES = dict(
        (c, [getattr(QROOT, "TH{0}{1}".format(d, c)) for d in (1, 2, 3)])
            for c in "CSIFD")

    def _parse_args(self, args, ignore_extras=False):
        params = [{
            'bins': None,
            'nbins': None,
            'low': None,
            'high': None} for _ in range(dim(self))]

        for param in params:
            if len(args) == 0:
                raise TypeError("did not receive expected number of arguments")
            if hasattr(args[0], '__iter__'):
                edges = list(args[0])
                if len(edges) < 2:
                    raise ValueError(
                        "specify at least two bin edges")
                if sorted(edges) != edges:
                    raise ValueError(
                        "bin edges must be sorted in ascending order")
                if len(set(args[0])) != len(args[0]):
                    raise ValueError("bin edges must not be repeated")
                param['bins'] = args[0]
                param['nbins'] = len(args[0]) - 1
                args = args[1:]
            elif len(args) >= 3:
                nbins = args[0]
                if not isinstance(nbins, int):
                    raise TypeError(
                        "number of bins must be an integer")
                if nbins < 1:
                    raise ValueError(
                        "number of bins must be positive")
                low = args[1]
                if not isinstance(low, numbers.Real):
                    raise TypeError(
                        "lower bound must be an int, float, or long")
                high = args[2]
                if not isinstance(high, numbers.Real):
                    raise TypeError(
                        "upper bound must be an int, float, or long")
                param['nbins'] = nbins
                param['low'] = low
                param['high'] = high
                if low >= high:
                    raise ValueError(
                        "upper bound (you gave {0:f}) "
                        "must be greater than lower "
                        "bound (you gave {1:f})".format(
                            float(low), float(high)))
                args = args[3:]
            else:
                raise TypeError(
                    "did not receive expected number of arguments")

        if ignore_extras:
            # used by Profile where range of profiled axis may be specified
            return params, args

        if len(args) != 0:
            raise TypeError(
                "did not receive expected number of arguments")

        return params

    def xyz(self, idx):
        """
        return binx, biny, binz corresponding to the global bin number
        """
        # Not implemented for Python 3:
        # GetBinXYZ(i, x, y, z)
        nx  = self.GetNbinsX() + 2
        ny  = self.GetNbinsY() + 2
        ndim = self.GetDimension()
        if ndim < 2:
            binx = idx % nx
            biny = 0
            binz = 0
        elif ndim < 3:
            binx = idx % nx
            biny = ((idx - binx) // nx) % ny
            binz = 0
        elif ndim < 4:
            binx = idx % nx
            biny = ((idx - binx) // nx) % ny
            binz = ((idx - binx) // nx - biny) // ny
        else:
            raise NotImplementedError
        return binx, biny, binz

    def axis_bininfo(self, axi, i):
        class bi:
            axis = self.axis(axi)
            low = axis.GetBinLowEdge(i)
            center = axis.GetBinCenter(i)
            high = axis.GetBinUpEdge(i)
            width = axis.GetBinWidth(i)
        return bi

    def bins(self, idx=None, overflow=False):
        if idx is None:
            idx = range(self.GetSize())
        elif isinstance(idx, slice):
            idx = range(*idx.indices(self.GetSize()))
            overflow = True
        else:
            idx = [self._range_check(idx)]
            overflow = True
        for i in idx:
            bproxy = BinProxy(self, i)
            if not overflow and bproxy.overflow:
                continue
            yield bproxy

    def bins_xyz(self, ix, iy=0, iz=0, proxy=True):
        xl = self.nbins(axis=0, overflow=True)
        yl = self.nbins(axis=1, overflow=True)
        zl = self.nbins(axis=2, overflow=True)
        if isinstance(ix, slice):
            ix = range(*ix.indices(xl))
        else:
            ix = [self._range_check(ix, axis=0)]
        if isinstance(iy, slice):
            iy = range(*iy.indices(yl))
        else:
            iy = [self._range_check(iy, axis=1)]
        if isinstance(iz, slice):
            iz = range(*iz.indices(zl))
        else:
            iz = [self._range_check(iz, axis=2)]
        if proxy:
            for x, y, z in product(ix, iy, iz):
                yield BinProxy(self, xl * yl * z + xl * y + x)
        else:
            for point in product(ix, iy, iz):
                yield point

    @classmethod
    def divide(cls, h1, h2, c1=1., c2=1., option='', fill_value=None):
        if hasattr(h1, 'GetHistogram'):
            h1 = h1.GetHistogram()
        if hasattr(h2, 'GetHistogram'):
            h2 = h2.GetHistogram()
        ratio = h1.Clone()
        ROOT.TH1.Divide(ratio, h1, h2, c1, c2, option)
        if fill_value is not None:
            for ratiobin, h2bin in zip(ratio.bins(), h2.bins()):
                if h2bin.value == 0:
                    ratiobin.value = fill_value
        return ratio

    def nbins(self, axis=0, overflow=False):
        """
        Get the number of bins along an axis
        """
        if axis == 0:
            nbins = self.GetNbinsX()
        elif axis == 1:
            nbins = self.GetNbinsY()
        elif axis == 2:
            nbins = self.GetNbinsZ()
        else:
            raise ValueError("axis must be 0, 1, or 2")
        if overflow:
            nbins += 2
        return nbins

    def bins_range(self, axis=0, overflow=False):
        """
        Return a range of bin indices for iterating along an axis

        Parameters
        ----------

        axis : int, optional (default=1)
            The axis (0, 1 or 2).

        overflow : bool, optional (default=False)
            If True then include the underflow and overflow bins
            otherwise only include the visible bins.

        Returns
        -------

        an range object of bin indices

        """
        nbins = self.nbins(axis=axis, overflow=False)
        if overflow:
            start = 0
            end_offset = 2
        else:
            start = 1
            end_offset = 1
        return range(start, nbins + end_offset)

    @property
    def axes(self):
        return [self.axis(i) for i in range(self.GetDimension())]

    def axis(self, axis=0):
        if axis == 0:
            return self.GetXaxis()
        elif axis == 1:
            return self.GetYaxis()
        elif axis == 2:
            return self.GetZaxis()
        else:
            raise ValueError("axis must be 0, 1, or 2")

    @property
    def entries(self):
        return self.GetEntries()

    @entries.setter
    def entries(self, value):
        self.SetEntries(value)

    def __len__(self):
        """
        The total number of bins, including overflow bins
        """
        return self.GetSize()

    def __iter__(self):
        """
        Iterate over the bin proxies
        """
        return self.bins(overflow=True)

    def _range_check(self, index, axis=None):

        if axis is None:
            size = self.GetSize()
        else:
            size = self.nbins(axis=axis)
            if axis < self.GetDimension():
                size += 2
        try:
            if index < 0:
                if index < - size:
                    raise IndexError
                return index % size
            elif index >= size:
                raise IndexError
        except IndexError:
            if axis is None:
                raise IndexError(
                    "global bin index {0:d} is out of range".format(index))
            else:
                raise IndexError(
                    "bin index {0:d} along axis {1:d} is out of range".format(
                        index, axis))
        return index

    def GetBin(self, ix, iy=0, iz=0):
        ix = self._range_check(ix, axis=0)
        iy = self._range_check(iy, axis=1)
        iz = self._range_check(iz, axis=2)
        return super(_HistBase, self).GetBin(ix, iy, iz)

    def __getitem__(self, index):
        """
        Return a BinProxy or list of BinProxies if index is a slice.
        """
        if isinstance(index, slice):
            if isinstance(self, _Hist):
                return HistView(self, index)
            return HistIndexView(self, index)
        if isinstance(index, tuple):
            ix, iy, iz = 0, 0, 0
            ndim = self.GetDimension()
            view = False
            if ndim == 2:
                try:
                    ix, iy = index
                except ValueError:
                    raise IndexError(
                        "must index along only two "
                        "axes of a 2D histogram")
                if isinstance(ix, slice) or isinstance(iy, slice):
                    return Hist2DView(self, x=ix, y=iy)
            elif ndim == 3:
                try:
                    ix, iy, iz = index
                except ValueError:
                    raise IndexError(
                        "must index along exactly three "
                        "axes of a 3D histogram")
                if (isinstance(ix, slice) or isinstance(iy, slice)
                        or isinstance(iz, slice)):
                    return Hist3DView(self, x=ix, y=iy, z=iz)
            else:
                raise IndexError(
                    "must index along only one "
                    "axis of a 1D histogram")
            index = self.GetBin(ix, iy, iz)
        else:
            index = self._range_check(index)
        return BinProxy(self, index)

    def __setitem__(self, index, value):
        """
        Set bin contents and additionally bin errors if value is a BinProxy or
        a 2-tuple containing the value and error.
        If index is a slice then value must be a list of values, BinProxies, or
        2-tuples of the same length as the slice.
        """
        is_slice = isinstance(index, slice)
        is_tuple = (not is_slice) and isinstance(index, tuple)
        if is_slice or is_tuple:

            if isinstance(value, _HistBase):
                self[index] = value[index]
                return

            if is_slice:
                indices = range(*index.indices(self.GetSize()))

            else:
                ndim = self.GetDimension()
                xl = self.nbins(0, overflow=True)
                yl = self.nbins(1, overflow=True)
                if ndim == 2:
                    try:
                        ix, iy = index
                    except ValueError:
                        raise IndexError(
                            "must index along only two "
                            "axes of a 2D histogram")
                    if isinstance(ix, slice):
                        ix = range(*ix.indices(xl))
                    else:
                        ix = [self._range_check(ix, axis=0)]
                    if isinstance(iy, slice):
                        iy = range(*iy.indices(yl))
                    else:
                        iy = [self._range_check(iy, axis=1)]
                    iz = [0]
                elif ndim == 3:
                    try:
                        ix, iy, iz = index
                    except ValueError:
                        raise IndexError(
                            "must index along exactly three "
                            "axes of a 3D histogram")
                    if isinstance(ix, slice):
                        ix = range(*ix.indices(xl))
                    else:
                        ix = [self._range_check(ix, axis=0)]
                    if isinstance(iy, slice):
                        iy = range(*iy.indices(yl))
                    else:
                        iy = [self._range_check(iy, axis=1)]
                    if isinstance(iz, slice):
                        iz = range(*iz.indices(self.nbins(2, overflow=True)))
                    else:
                        iz = [self._range_check(iz, axis=2)]
                else:
                    raise IndexError(
                        "must index along only one "
                        "axis of a 1D histogram")
                indices = (xl * yl * z + xl * y + x
                    for (x, y, z) in product(ix, iy, iz))

            if isinstance(value, _HistViewBase):
                for i, v in izip_exact(indices, value):
                    self.SetBinContent(i, v.value)
                    self.SetBinError(i, v.error)
            elif hasattr(value, '__iter__') and not isinstance(value, tuple):
                if value and isinstance(value[0], tuple):
                    for i, v in izip_exact(indices, value):
                        _value, _error = value
                        self.SetBinContent(i, _value)
                        self.SetBinError(i, _error)
                else:
                    for i, v in izip_exact(indices, value):
                        self.SetBinContent(i, v)
            elif isinstance(value, BinProxy):
                v, e = value.value, value.error
                for i in indices:
                    self.SetBinContent(i, v)
                    self.SetBinError(i, e)
            elif isinstance(value, tuple):
                _value, _error = value
                for i in indices:
                    self.SetBinContent(i, _value)
                    self.SetBinError(i, _error)
            else:
                for i in indices:
                    self.SetBinContent(i, value)
            return

        index = self._range_check(index)

        if isinstance(value, BinProxy):
            self.SetBinContent(index, value.value)
            self.SetBinError(index, value.error)
        elif isinstance(value, tuple):
            value, error = value
            self.SetBinContent(index, value)
            self.SetBinError(index, error)
        else:
            self.SetBinContent(index, value)

    def uniform(self, axis=None, precision=1E-7):
        """
        Return True if the binning is uniform along the specified axis.
        If axis is None (the default), then return True if the binning is
        uniform along all axes. Otherwise return False.

        Parameters
        ----------

        axis : int (default=None)
            Axis along which to check if the binning is uniform. If None,
            then check all axes.

        precision : float (default=1E-7)
            The threshold below which differences in bin widths are ignored and
            treated as equal.

        Returns
        -------

        True if the binning is uniform, otherwise False.

        """
        if axis is None:
            for axis in range(self.GetDimension()):
                widths = list(self._width(axis=axis))
                if not all(abs(x - widths[0]) < precision for x in widths):
                    return False
            return True
        widths = list(self._width(axis=axis))
        return all(abs(x - widths[0]) < precision for x in widths)

    def uniform_binned(self, name=None):
        """
        Return a new histogram with constant width bins along all axes by
        using the bin indices as the bin edges of the new histogram.
        """
        if self.GetDimension() == 1:
            new_hist = Hist(
                self.GetNbinsX(), 0, self.GetNbinsX(),
                name=name, type=self.TYPE)
        elif self.GetDimension() == 2:
            new_hist = Hist2D(
                self.GetNbinsX(), 0, self.GetNbinsX(),
                self.GetNbinsY(), 0, self.GetNbinsY(),
                name=name, type=self.TYPE)
        else:
            new_hist = Hist3D(
                self.GetNbinsX(), 0, self.GetNbinsX(),
                self.GetNbinsY(), 0, self.GetNbinsY(),
                self.GetNbinsZ(), 0, self.GetNbinsZ(),
                name=name, type=self.TYPE)
        # copy over the bin contents and errors
        for outbin, inbin in zip(new_hist.bins(), self.bins()):
            outbin.value = inbin.value
            outbin.error = inbin.error
        new_hist.decorate(self)
        new_hist.entries = self.entries
        return new_hist

    def underflow(self, axis=0):
        """
        Return the underflow for the given axis.

        Depending on the dimension of the histogram, may return an array.
        """
        if axis not in range(3):
            raise ValueError("axis must be 0, 1, or 2")
        if self.DIM == 1:
            return self.GetBinContent(0)
        elif self.DIM == 2:
            def idx(i):
                arg = [i]
                arg.insert(axis, 0)
                return arg
            return [
                self.GetBinContent(*idx(i))
                for i in self.bins_range(axis=(axis + 1) % 2, overflow=True)]
        elif self.DIM == 3:
            axes = [0, 1, 2]
            axes.remove(axis)
            axis2, axis3 = axes
            def idx(i, j):
                arg = [i, j]
                arg.insert(axis, 0)
                return arg
            return [[
                self.GetBinContent(*idx(i, j))
                for i in self.bins_range(axis=axis2, overflow=True)]
                for j in self.bins_range(axis=axis3, overflow=True)]

    def overflow(self, axis=0):
        """
        Return the overflow for the given axis.

        Depending on the dimension of the histogram, may return an array.
        """
        if axis not in range(3):
            raise ValueError("axis must be 0, 1, or 2")
        if self.DIM == 1:
            return self.GetBinContent(self.nbins(0) + 1)
        elif self.DIM == 2:
            axes = [0, 1]
            axes.remove(axis)
            axis2 = axes[0]
            nbins_axis = self.nbins(axis)
            def idx(i):
                arg = [i]
                arg.insert(axis, nbins_axis + 1)
                return arg
            return [
                self.GetBinContent(*idx(i))
                for i in self.bins_range(axis=axis2, overflow=True)]
        elif self.DIM == 3:
            axes = [0, 1, 2]
            axes.remove(axis)
            axis2, axis3 = axes
            nbins_axis = self.nbins(axis)
            def idx(i, j):
                arg = [i, j]
                arg.insert(axis, nbins_axis + 1)
                return arg
            return [[
                self.GetBinContent(*idx(i, j))
                for i in self.bins_range(axis=axis2, overflow=True)]
                for j in self.bins_range(axis=axis3, overflow=True)]

    def lowerbound(self, axis=0):
        """
        Get the lower bound of the binning along an axis
        """
        if not 0 <= axis < self.GetDimension():
            raise ValueError(
                "axis must be a non-negative integer less than "
                "the dimensionality of the histogram")
        if axis == 0:
            return self.xedges(1)
        if axis == 1:
            return self.yedges(1)
        if axis == 2:
            return self.zedges(1)
        raise TypeError("axis must be an integer")

    def upperbound(self, axis=0):
        """
        Get the upper bound of the binning along an axis
        """
        if not 0 <= axis < self.GetDimension():
            raise ValueError(
                "axis must be a non-negative integer less than "
                "the dimensionality of the histogram")
        if axis == 0:
            return self.xedges(-2)
        if axis == 1:
            return self.yedges(-2)
        if axis == 2:
            return self.zedges(-2)
        raise TypeError("axis must be an integer")

    def bounds(self, axis=0):
        """
        Get the lower and upper bounds of the binning along an axis
        """
        if not 0 <= axis < self.GetDimension():
            raise ValueError(
                "axis must be a non-negative integer less than "
                "the dimensionality of the histogram")
        if axis == 0:
            return self.xedges(1), self.xedges(-2)
        if axis == 1:
            return self.yedges(1), self.yedges(-2)
        if axis == 2:
            return self.zedges(1), self.zedges(-2)
        raise TypeError("axis must be an integer")

    def _centers(self, axis, index=None, overflow=False):
        nbins = self.nbins(axis)
        ax = self.axis(axis)
        if index is None:
            def temp_generator():
                if overflow:
                    yield float('-inf')
                for index in range(1, nbins + 1):
                    yield ax.GetBinCenter(index)
                if overflow:
                    yield float('+inf')
            return temp_generator()
        index = index % (nbins + 2)
        if index == 0:
            return float('-inf')
        elif index == nbins + 1:
            return float('+inf')
        return ax.GetBinCenter(index)

    def _edgesl(self, axis, index=None, overflow=False):
        nbins = self.nbins(axis)
        ax = self.axis(axis)
        if index is None:
            def temp_generator():
                if overflow:
                    yield float('-inf')
                for index in range(1, nbins + 1):
                    yield ax.GetBinLowEdge(index)
                if overflow:
                    yield ax.GetBinUpEdge(index)
            return temp_generator()
        index = index % (nbins + 2)
        if index == 0:
            return float('-inf')
        return ax.GetBinLowEdge(index)

    def _edgesh(self, axis, index=None, overflow=False):
        nbins = self.nbins(axis)
        ax = self.axis(axis)
        if index is None:
            def temp_generator():
                if overflow:
                    yield ax.GetBinUpEdge(0)
                for index in range(1, nbins + 1):
                    yield ax.GetBinUpEdge(index)
                if overflow:
                    yield float('+inf')
            return temp_generator()
        index = index % (nbins + 2)
        if index == nbins + 1:
            return float('+inf')
        return ax.GetBinUpEdge(index)

    def _edges(self, axis, index=None, overflow=False):
        nbins = self.nbins(axis)
        ax = self.axis(axis)
        if index is None:
            def temp_generator():
                if overflow:
                    yield float('-inf')
                for index in range(1, nbins + 1):
                    yield ax.GetBinLowEdge(index)
                yield ax.GetBinUpEdge(nbins)
                if overflow:
                    yield float('+inf')
            return temp_generator()
        index = index % (nbins + 3)
        if index == 0:
            return float('-inf')
        if index == nbins + 2:
            return float('+inf')
        return ax.GetBinLowEdge(index)

    def _width(self, axis, index=None, overflow=False):
        nbins = self.nbins(axis)
        ax = self.axis(axis)
        if index is None:
            def temp_generator():
                if overflow:
                    yield float('+inf')
                for index in range(1, nbins + 1):
                    yield ax.GetBinWidth(index)
                if overflow:
                    yield float('+inf')
            return temp_generator()
        index = index % (nbins + 2)
        if index in (0, nbins + 1):
            return float('+inf')
        return ax.GetBinWidth(index)

    def _erravg(self, axis, index=None, overflow=False):
        nbins = self.nbins(axis)
        ax = self.axis(axis)
        if index is None:
            def temp_generator():
                if overflow:
                    yield float('+inf')
                for index in range(1, nbins + 1):
                    yield ax.GetBinWidth(index) / 2.
                if overflow:
                    yield float('+inf')
            return temp_generator()
        index = index % (nbins + 2)
        if index in (0, nbins + 1):
            return float('+inf')
        return ax.GetBinWidth(index) / 2.

    def _err(self, axis, index=None, overflow=False):
        nbins = self.nbins(axis)
        ax = self.axis(axis)
        if index is None:
            def temp_generator():
                if overflow:
                    yield (float('+inf'), float('+inf'))
                for index in range(1, nbins + 1):
                    w = ax.GetBinWidth(index) / 2.
                    yield (w, w)
                if overflow:
                    yield (float('+inf'), float('+inf'))
            return temp_generator()
        index = index % (nbins + 2)
        if index in (0, nbins + 1):
            return (float('+inf'), float('+inf'))
        w = ax.GetBinWidth(index) / 2.
        return (w, w)

    def check_compatibility(self, other, check_edges=False, precision=1E-7):
        """
        Test whether two histograms are considered compatible by the number of
        dimensions, number of bins along each axis, and optionally the bin
        edges.

        Parameters
        ----------

        other : histogram
            A rootpy histogram

        check_edges : bool, optional (default=False)
            If True then also check that the bin edges are equal within
            the specified precision.

        precision : float, optional (default=1E-7)
            The value below which differences between floats are treated as
            nil when comparing bin edges.

        Raises
        ------

        TypeError
            If the histogram dimensionalities do not match

        ValueError
            If the histogram sizes, number of bins along an axis, or
            optionally the bin edges do not match

        """
        if self.GetDimension() != other.GetDimension():
            raise TypeError("histogram dimensionalities do not match")
        if len(self) != len(other):
            raise ValueError("histogram sizes do not match")
        for axis in range(self.GetDimension()):
            if self.nbins(axis=axis) != other.nbins(axis=axis):
                raise ValueError(
                    "numbers of bins along axis {0:d} do not match".format(
                        axis))
        if check_edges:
            for axis in range(self.GetDimension()):
                if not all([abs(l - r) < precision
                    for l, r in zip(self._edges(axis), other._edges(axis))]):
                    raise ValueError(
                        "edges do not match along axis {0:d}".format(axis))

    def compatible(self, other, check_edges=False, precision=1E-7):
        try:
            self.check_compatibility(other,
                check_edges=check_edges, precision=precision)
        except (TypeError, ValueError):
            return False
        return True

    def __add__(self, other):
        copy = self.Clone()
        copy += other
        return copy

    def __iadd__(self, other):
        if isinstance(other, numbers.Real):
            if other != 0:
                for bin in self.bins(overflow=True):
                    bin.value += other
        else:
            self.Add(other)
        return self

    def __radd__(self, other):
        if isinstance(other, numbers.Real):
            copy = self.Clone()
            if other != 0:
                copy += other
            return copy
        return NotImplemented

    def __sub__(self, other):
        copy = self.Clone()
        copy -= other
        return copy

    def __isub__(self, other):
        if isinstance(other, numbers.Real):
            if other != 0:
                for bin in self.bins(overflow=True):
                    bin.value -= other
        else:
            self.Add(other, -1.)
        return self

    def __rsub__(self, other):
        if isinstance(other, numbers.Real):
            copy = self.Clone()
            if other != 0:
                for bin in copy.bins(overflow=True):
                    bin.value = other - bin.value
            return copy
        return NotImplemented

    def __mul__(self, other):
        copy = self.Clone()
        copy *= other
        return copy

    def __imul__(self, other):
        if isinstance(other, numbers.Real):
            self.Scale(other)
            return self
        self.Multiply(other)
        return self

    def __rmul__(self, other):
        if isinstance(other, numbers.Real):
            copy = self.Clone()
            if other != 1:
                copy *= other
            return copy
        return NotImplemented

    def __div__(self, other):
        copy = self.Clone()
        copy /= other
        return copy

    __truediv__ = __div__

    def __idiv__(self, other):
        if isinstance(other, numbers.Real):
            if other == 0:
                raise ZeroDivisionError(
                    "attempting to divide histogram by zero")
            self.Scale(1. / other)
            return self
        self.Divide(other)
        return self

    __itruediv__ = __idiv__

    def __rdiv__(self, other):
        if isinstance(other, numbers.Real):
            copy = self.Clone()
            for bin in copy.bins(overflow=True):
                v = bin.value
                if v != 0:
                    bin.value = other / v
            return copy
        return NotImplemented

    __rtruediv__ = __rdiv__

    def __ipow__(self, other, modulo=None):
        if modulo is not None:
            return NotImplemented
        if isinstance(other, numbers.Real):
            for bin in self.bins(overflow=True):
                bin **= other
        elif isinstance(other, _HistBase):
            self.check_compatibility(other)
            for this_bin, other_bin in zip(
                    self.bins(overflow=True),
                    other.bins(overflow=True)):
                this_bin **= other_bin.value
        else:
            return NotImplemented
        return self

    def __pow__(self, other, modulo=None):
        if modulo is not None:
            return NotImplemented
        copy = self.Clone()
        copy **= other
        return copy

    def fill_array(self, array, weights=None):
        """
        Fill this histogram with a NumPy array
        """
        try:
            try:
                from root_numpy import fill_hist as fill_func
            except ImportError:
                from root_numpy import fill_array as fill_func
        except ImportError:
            log.critical(
                "root_numpy is needed for Hist*.fill_array. "
                "Is it installed and importable?")
            raise
        fill_func(self, array, weights=weights)

    def fill_view(self, view):
        """
        Fill this histogram from a view of another histogram
        """
        other = view.hist
        _other_x_center = other.axis(0).GetBinCenter
        _other_y_center = other.axis(1).GetBinCenter
        _other_z_center = other.axis(2).GetBinCenter
        _other_get = other.GetBinContent
        _other_get_bin = super(_HistBase, other).GetBin
        other_sum_w2 = other.GetSumw2()
        _other_sum_w2_at = other_sum_w2.At

        _find = self.FindBin
        sum_w2 = self.GetSumw2()
        _sum_w2_at = sum_w2.At
        _sum_w2_setat = sum_w2.SetAt
        _set = self.SetBinContent
        _get = self.GetBinContent

        for x, y, z in view.points:
            idx = _find(
                _other_x_center(x),
                _other_y_center(y),
                _other_z_center(z))
            other_idx = _other_get_bin(x, y, z)
            _set(idx, _get(idx) + _other_get(other_idx))
            _sum_w2_setat(
                _sum_w2_at(idx) + _other_sum_w2_at(other_idx),
                idx)

    def FillRandom(self, func, ntimes=5000):
        if isinstance(func, QROOT.TF1):
            func = func.GetName()
        super(_HistBase, self).FillRandom(func, ntimes)
        return self

    def get_sum_w2(self, ix, iy=0, iz=0):
        """
        Obtain the true number of entries in the bin weighted by w^2
        """
        if self.GetSumw2N() == 0:
            raise RuntimeError(
                "Attempting to access Sumw2 in histogram "
                "where weights were not stored")
        xl = self.nbins(axis=0, overflow=True)
        yl = self.nbins(axis=1, overflow=True)
        idx = xl * yl * iz + xl * iy + ix
        if not 0 <= idx < self.GetSumw2N():
            raise IndexError("bin index out of range")
        return self.GetSumw2().At(idx)

    def set_sum_w2(self, w, ix, iy=0, iz=0):
        """
        Sets the true number of entries in the bin weighted by w^2
        """
        if self.GetSumw2N() == 0:
            raise RuntimeError(
                "Attempting to access Sumw2 in histogram "
                "where weights were not stored")
        xl = self.nbins(axis=0, overflow=True)
        yl = self.nbins(axis=1, overflow=True)
        idx = xl * yl * iz + xl * iy + ix
        if not 0 <= idx < self.GetSumw2N():
            raise IndexError("bin index out of range")
        self.GetSumw2().SetAt(w, idx)

    def merge_bins(self, bin_ranges, axis=0):
        """
        Merge bins in bin ranges

        Parameters
        ----------

        bin_ranges : list of tuples
            A list of tuples of bin indices for each bin range to be merged
            into one bin.

        axis : int (default=1)
            The integer identifying the axis to merge bins along.

        Returns
        -------

        hist : TH1
            The rebinned histogram.

        Examples
        --------

        Merge the overflow bins into the first and last real bins::

            newhist = hist.merge_bins([(0, 1), (-2, -1)])

        """
        ndim = self.GetDimension()
        if axis > ndim - 1:
            raise ValueError(
                "axis is out of range")
        axis_bins = self.nbins(axis=axis, overflow=True)

        # collect the indices along this axis to be merged
        # support negative indices via slicing
        windows = []
        for window in bin_ranges:
            if len(window) != 2:
                raise ValueError(
                    "bin range tuples must contain "
                    "two elements: {0!r}".format(window))
            l, r = window
            if l == r:
                raise ValueError(
                    "bin indices must not be equal "
                    "in a merging window: {0!r}".format(window))
            if (l < 0 and r >= 0) or (l > 0 and r > 0 and l > r):
                raise ValueError(
                    "invalid bin range: {0!r}".format(window))
            if r == -1:
                r = axis_bins
            else:
                r += 1
            bin_idx = range(*slice(l, r).indices(axis_bins))
            if bin_idx: # skip []
                windows.append(list(bin_idx))

        if not windows:
            # no merging will take place so return a clone
            return self.Clone()

        # check that windows do not overlap
        if len(windows) > 1:
            flattened = [idx for window in windows for idx in window]
            if len(flattened) != len(set(flattened)):
                raise ValueError(
                    "bin index windows overlap: {0!r}".format(bin_ranges))

        # construct a mapping from old to new bin index along this axis
        windows.sort()
        mapping = {}
        left_idx = {}
        offset = 0
        for window in windows:
            # put underflow in first bin
            new_idx = window[0] - offset or 1
            left_idx[window[0] or 1] = None
            for idx in window:
                mapping[idx] = new_idx
            offset += len(window) - 1
            if window[0] == 0:
                offset -= 1

        new_axis_bins = axis_bins - offset

        # construct new bin edges
        new_edges = []
        for i, edge in enumerate(self._edges(axis)):
            if (i != axis_bins - 2 and i + 1 in mapping
                and i + 1 not in left_idx):
                continue
            new_edges.append(edge)

        # construct new histogram and fill
        new_hist = self.empty_clone(binning=new_edges, axis=axis)

        this_axis = self.axis(axis)
        new_axis = new_hist.axis(axis)

        def translate(idx):
            if idx in mapping:
                return mapping[idx]
            if idx == 0:
                return 0
            # use TH1.FindBin to determine where the bins should be merged
            return new_axis.FindBin(this_axis.GetBinCenter(idx))

        for bin in self.bins(overflow=True):
            xyz = bin.xyz
            new_xyz = list(xyz)
            new_xyz[axis] = translate(int(xyz[axis]))

            x, y, z = new_xyz

            new_v = new_hist.GetBinContent(x, y, z)
            new_hist.SetBinContent(x, y, z, new_v + bin.value)

            sum_w2 = self.get_sum_w2(*xyz)
            new_sum_w2 = new_hist.get_sum_w2(x, y, z)
            new_hist.set_sum_w2(sum_w2 + new_sum_w2, x, y, z)

        # transfer stats info
        stat_array = array('d', [0.] * 10)
        self.GetStats(stat_array)
        new_hist.PutStats(stat_array)
        entries = self.GetEntries()
        new_hist.SetEntries(entries)
        return new_hist

    def rebinned(self, bins, axis=0):
        """
        Return a new rebinned histogram

        Parameters
        ----------

        bins : int, tuple, or iterable
            If ``bins`` is an int, then return a histogram that is rebinned by
            grouping N=``bins`` bins together along the axis ``axis``.
            If ``bins`` is a tuple, then it must contain the same number of
            elements as there are dimensions of this histogram and each element
            will be used to rebin along the associated axis.
            If ``bins`` is another iterable, then it will define the bin
            edges along the axis ``axis`` in the new rebinned histogram.

        axis : int, optional (default=0)
            The axis to rebin along.

        Returns
        -------

        The rebinned histogram

        """
        ndim = self.GetDimension()
        if axis >= ndim:
            raise ValueError(
                "axis must be less than the dimensionality of the histogram")

        if isinstance(bins, int):
            _bins = [1] * ndim
            try:
                _bins[axis] = bins
            except IndexError:
                raise ValueError("axis must be 0, 1, or 2")
            bins = tuple(_bins)

        if isinstance(bins, tuple):
            if len(bins) != ndim:
                raise ValueError(
                    "bins must be a tuple with the same "
                    "number of elements as histogram axes")
            newname = '{0}_{1}'.format(self.__class__.__name__, uuid())
            if ndim == 1:
                hist = self.Rebin(bins[0], newname)
            elif ndim == 2:
                hist = self.Rebin2D(bins[0], bins[1], newname)
            else:
                hist = self.Rebin3D(bins[0], bins[1], bins[2], newname)
            hist = asrootpy(hist)
        elif hasattr(bins, '__iter__'):
            hist = self.empty_clone(bins, axis=axis)
            nbinsx = self.nbins(0)
            nbinsy = self.nbins(1)
            nbinsz = self.nbins(2)
            xaxis = self.xaxis
            yaxis = self.yaxis
            zaxis = self.zaxis
            sum_w2 = self.GetSumw2()
            _sum_w2_at = sum_w2.At
            new_sum_w2 = hist.GetSumw2()
            _new_sum_w2_at = new_sum_w2.At
            _new_sum_w2_setat = new_sum_w2.SetAt
            _x_center = xaxis.GetBinCenter
            _y_center = yaxis.GetBinCenter
            _z_center = zaxis.GetBinCenter
            _find = hist.FindBin
            _set = hist.SetBinContent
            _get = hist.GetBinContent
            _this_get = self.GetBinContent
            _get_bin = super(_HistBase, self).GetBin
            for z in range(1, nbinsz + 1):
                for y in range(1, nbinsy + 1):
                    for x in range(1, nbinsx + 1):
                        newbin = _find(
                            _x_center(x), _y_center(y), _z_center(z))
                        idx = _get_bin(x, y, z)
                        _set(newbin, _get(newbin) + _this_get(idx))
                        _new_sum_w2_setat(
                            _new_sum_w2_at(newbin) + _sum_w2_at(idx),
                            newbin)
            hist.SetEntries(self.GetEntries())
        else:
            raise TypeError(
                "bins must either be an integer, a tuple, or an iterable")
        return hist

    def smoothed(self, iterations=1):
        """
        Return a smoothed copy of this histogram

        Parameters
        ----------

        iterations : int, optional (default=1)
            The number of smoothing iterations

        Returns
        -------

        hist : asrootpy'd histogram
            The smoothed histogram

        """
        copy = self.Clone(shallow=True)
        copy.Smooth(iterations)
        return copy

    def empty_clone(self, binning=None, axis=0, type=None, **kwargs):
        """
        Return a new empty histogram. The binning may be modified
        along one axis by specifying the binning and axis arguments.
        If binning is False, then the corresponding axis is dropped
        from the returned histogram.
        """
        ndim = self.GetDimension()
        if binning is False and ndim == 1:
            raise ValueError(
                "cannot remove the x-axis of a 1D histogram")
        args = []
        for iaxis in range(ndim):
            if iaxis == axis:
                if binning is False:
                    # skip this axis
                    continue
                elif binning is not None:
                    if hasattr(binning, '__iter__'):
                        binning = (binning,)
                    args.extend(binning)
                    continue
            args.append(list(self._edges(axis=iaxis)))
        if type is None:
            type = self.TYPE
        if binning is False:
            ndim -= 1
        cls = [Hist, Hist2D, Hist3D][ndim - 1]
        return cls(*args, type=type, **kwargs)

    def quantiles(self, quantiles,
                  axis=0, strict=False,
                  recompute_integral=False):
        """
        Calculate the quantiles of this histogram.

        Parameters
        ----------

        quantiles : list or int
            A list of cumulative probabilities or an integer used to determine
            equally spaced values between 0 and 1 (inclusive).

        axis : int, optional (default=0)
            The axis to compute the quantiles along. 2D and 3D histograms are
            first projected along the desired axis before computing the
            quantiles.

        strict : bool, optional (default=False)
            If True, then return the sorted unique quantiles corresponding
            exactly to bin edges of this histogram.

        recompute_integral : bool, optional (default=False)
            If this histogram was filled with SetBinContent instead of Fill,
            then the integral must be computed before calculating the
            quantiles.

        Returns
        -------

        output : list or numpy array
            If NumPy is importable then an array of the quantiles is returned,
            otherwise a list is returned.

        """
        if axis >= self.GetDimension():
            raise ValueError(
                "axis must be less than the dimensionality of the histogram")
        if recompute_integral:
            self.ComputeIntegral()
        if isinstance(self, _Hist2D):
            newname = '{0}_{1}'.format(self.__class__.__name__, uuid())
            if axis == 0:
                proj = self.ProjectionX(newname, 1, self.nbins(1))
            elif axis == 1:
                proj = self.ProjectionY(newname, 1, self.nbins(0))
            else:
                raise ValueError("axis must be 0 or 1")
            return asrootpy(proj).quantiles(
                quantiles, strict=strict, recompute_integral=False)
        elif isinstance(self, _Hist3D):
            newname = '{0}_{1}'.format(self.__class__.__name__, uuid())
            if axis == 0:
                proj = self.ProjectionX(
                    newname, 1, self.nbins(1), 1, self.nbins(2))
            elif axis == 1:
                proj = self.ProjectionY(
                    newname, 1, self.nbins(0), 1, self.nbins(2))
            elif axis == 2:
                proj = self.ProjectionZ(
                    newname, 1, self.nbins(0), 1, self.nbins(1))
            else:
                raise ValueError("axis must be 0, 1, or 2")
            return asrootpy(proj).quantiles(
                quantiles, strict=strict, recompute_integral=False)
        try:
            import numpy as np
        except ImportError:
            # use python implementation
            use_numpy = False
        else:
            use_numpy = True
        if isinstance(quantiles, int):
            num_quantiles = quantiles
            if use_numpy:
                qs = np.linspace(0, 1, num_quantiles)
                output = np.empty(num_quantiles, dtype=float)
            else:
                def linspace(start, stop, n):
                    if n == 1:
                        yield start
                        return
                    h = float(stop - start) / (n - 1)
                    for i in range(n):
                        yield start + h * i
                quantiles = list(linspace(0, 1, num_quantiles))
                qs = array('d', quantiles)
                output = array('d', [0.] * num_quantiles)
        else:
            num_quantiles = len(quantiles)
            if use_numpy:
                qs = np.array(quantiles, dtype=float)
                output = np.empty(num_quantiles, dtype=float)
            else:
                qs = array('d', quantiles)
                output = array('d', [0.] * num_quantiles)
        if strict:
            integral = self.GetIntegral()
            nbins = self.nbins(0)
            if use_numpy:
                edges = np.empty(nbins + 1, dtype=float)
                self.GetLowEdge(edges)
                edges[-1] = edges[-2] + self.GetBinWidth(nbins)
                integral = np.ndarray((nbins + 1,), dtype=float, buffer=integral)
                idx = np.searchsorted(integral, qs, side='left')
                output = np.unique(np.take(edges, idx))
            else:
                quantiles = list(set(qs))
                quantiles.sort()
                output = []
                ibin = 0
                for quant in quantiles:
                    # find first bin greater than or equal to quant
                    while integral[ibin] < quant and ibin < nbins + 1:
                        ibin += 1
                    edge = self.GetBinLowEdge(ibin + 1)
                    output.append(edge)
                    if ibin >= nbins + 1:
                        break
                output = list(set(output))
                output.sort()
            return output
        self.GetQuantiles(num_quantiles, output, qs)
        if use_numpy:
            return output
        return list(output)

    def max(self, include_error=False):
        if not include_error:
            return self.GetBinContent(self.GetMaximumBin())
        clone = self.Clone(shallow=True)
        for i in range(self.GetSize()):
            clone.SetBinContent(
                i, clone.GetBinContent(i) + clone.GetBinError(i))
        return clone.GetBinContent(clone.GetMaximumBin())

    def min(self, include_error=False):
        if not include_error:
            return self.GetBinContent(self.GetMinimumBin())
        clone = self.Clone(shallow=True)
        for i in range(self.GetSize()):
            clone.SetBinContent(
                i, clone.GetBinContent(i) - clone.GetBinError(i))
        return clone.GetBinContent(clone.GetMinimumBin())


class _Hist(_HistBase):

    DIM = 1

    def x(self, index=None, overflow=False):
        return self._centers(0, index, overflow=overflow)

    def xerravg(self, index=None, overflow=False):
        return self._erravg(0, index, overflow=overflow)

    def xerrl(self, index=None, overflow=False):
        return self._erravg(0, index, overflow=overflow)

    def xerrh(self, index=None, overflow=False):
        return self._erravg(0, index, overflow=overflow)

    def xerr(self, index=None, overflow=False):
        return self._err(0, index, overflow=overflow)

    def xwidth(self, index=None, overflow=False):
        return self._width(0, index, overflow=overflow)

    def xedgesl(self, index=None, overflow=False):
        return self._edgesl(0, index, overflow=overflow)

    def xedgesh(self, index=None, overflow=False):
        return self._edgesh(0, index, overflow=overflow)

    def xedges(self, index=None, overflow=False):
        return self._edges(0, index, overflow=overflow)

    def yerrh(self, index=None, overflow=False):
        return self.yerravg(index, overflow=overflow)

    def yerrl(self, index=None, overflow=False):
        return self.yerravg(index, overflow=overflow)

    def y(self, index=None, overflow=False):
        if index is None:
            return (self.GetBinContent(i)
                    for i in self.bins_range(axis=0, overflow=overflow))
        index = index % self.nbins(axis=0, overflow=True)
        return self.GetBinContent(index)

    def yerravg(self, index=None, overflow=False):
        if index is None:
            return (self.GetBinError(i)
                    for i in self.bins_range(axis=0, overflow=overflow))
        index = index % self.nbins(axis=0, overflow=True)
        return self.GetBinError(index)

    def yerr(self, index=None, overflow=False):
        if index is None:
            return ((self.yerrl(i), self.yerrh(i))
                    for i in self.bins_range(axis=0, overflow=overflow))
        index = index % self.nbins(axis=0, overflow=True)
        return (self.yerrl(index), self.yerrh(index))

    def expectation(self, startbin=1, endbin=None):
        if endbin is not None and endbin < startbin:
            raise ValueError("``endbin`` should be greated than ``startbin``")
        if endbin is None:
            endbin = self.nbins(0)
        expect = 0.
        norm = 0.
        for index in range(startbin, endbin + 1):
            val = self[index]
            expect += val * self.x(index)
            norm += val
        if norm > 0:
            return expect / norm
        else:
            return (self.xedges(endbin + 1) + self.xedges(startbin)) / 2

    def integral(self, xbin1=None, xbin2=None,
                 width=False, error=False, overflow=False):
        """
        Compute the integral and error over a range of bins
        """
        if xbin1 is None:
            xbin1 = 0 if overflow else 1
        if xbin2 is None:
            xbin2 = -1 if overflow else -2
        nbinsx = self.nbins(axis=0, overflow=True)
        xbin1 %= nbinsx
        xbin2 %= nbinsx
        options = 'width' if width else ''
        if error:
            error = ROOT.Double()
            integral = super(_Hist, self).IntegralAndError(
                xbin1, xbin2, error, options)
            return integral, error
        return super(_Hist, self).Integral(xbin1, xbin2, options)

    def poisson_errors(self):
        """
        Return a TGraphAsymmErrors representation of this histogram where the
        point y errors are Poisson.
        """
        graph = Graph(self.nbins(axis=0), type='asymm')
        graph.SetLineWidth(self.GetLineWidth())
        graph.SetMarkerSize(self.GetMarkerSize())
        chisqr = ROOT.TMath.ChisquareQuantile
        npoints = 0
        for bin in self.bins(overflow=False):
            entries = bin.effective_entries
            if entries <= 0:
                continue
            ey_low = entries - 0.5 * chisqr(0.1586555, 2. * entries)
            ey_high = 0.5 * chisqr(
                1. - 0.1586555, 2. * (entries + 1)) - entries
            ex = bin.x.width / 2.
            graph.SetPoint(npoints, bin.x.center, bin.value)
            graph.SetPointEXlow(npoints, ex)
            graph.SetPointEXhigh(npoints, ex)
            graph.SetPointEYlow(npoints, ey_low)
            graph.SetPointEYhigh(npoints, ey_high)
            npoints += 1
        graph.Set(npoints)
        return graph


class _Hist2D(_HistBase):

    DIM = 2

    def x(self, index=None, overflow=False):
        return self._centers(0, index, overflow=overflow)

    def xerravg(self, index=None, overflow=False):
        return self._erravg(0, index, overflow=overflow)

    def xerrl(self, index=None, overflow=False):
        return self._erravg(0, index, overflow=overflow)

    def xerrh(self, index=None, overflow=False):
        return self._erravg(0, index, overflow=overflow)

    def xerr(self, index=None, overflow=False):
        return self._err(0, index, overflow=overflow)

    def xwidth(self, index=None, overflow=False):
        return self._width(0, index, overflow=overflow)

    def xedgesl(self, index=None, overflow=False):
        return self._edgesl(0, index, overflow=overflow)

    def xedgesh(self, index=None, overflow=False):
        return self._edgesh(0, index, overflow=overflow)

    def xedges(self, index=None, overflow=False):
        return self._edges(0, index, overflow=overflow)

    def y(self, index=None, overflow=False):
        return self._centers(1, index, overflow=overflow)

    def yerravg(self, index=None, overflow=False):
        return self._erravg(1, index, overflow=overflow)

    def yerrl(self, index=None, overflow=False):
        return self._erravg(1, index, overflow=overflow)

    def yerrh(self, index=None, overflow=False):
        return self._erravg(1, index, overflow=overflow)

    def yerr(self, index=None, overflow=False):
        return self._err(1, index, overflow=overflow)

    def ywidth(self, index=None, overflow=False):
        return self._width(1, index, overflow=overflow)

    def yedgesl(self, index=None, overflow=False):
        return self._edgesl(1, index, overflow=overflow)

    def yedgesh(self, index=None, overflow=False):
        return self._edgesh(1, index, overflow=overflow)

    def yedges(self, index=None, overflow=False):
        return self._edges(1, index, overflow=overflow)

    def zerrh(self, index=None, overflow=False):
        return self.zerravg(index, overflow=overflow)

    def zerrl(self, index=None, overflow=False):
        return self.zerravg(index, overflow=overflow)

    def z(self, ix=None, iy=None, overflow=False):
        if ix is None and iy is None:
            return [[self.GetBinContent(ix, iy)
                    for iy in self.bins_range(axis=1, overflow=overflow)]
                    for ix in self.bins_range(axis=0, overflow=overflow)]
        ix = ix % self.nbins(axis=0, overflow=True)
        iy = iy % self.nbins(axis=1, overflow=True)
        return self.GetBinContent(ix, iy)

    def zerravg(self, ix=None, iy=None, overflow=False):
        if ix is None and iy is None:
            return [[self.GetBinError(ix, iy)
                    for iy in self.bins_range(axis=1, overflow=overflow)]
                    for ix in self.bins_range(axis=0, overflow=overflow)]
        ix = ix % self.nbins(axis=0, overflow=True)
        iy = iy % self.nbins(axis=1, overflow=True)
        return self.GetBinError(ix, iy)

    def zerr(self, ix=None, iy=None, overflow=False):
        if ix is None and iy is None:
            return [[(self.GetBinError(ix, iy), self.GetBinError(ix, iy))
                    for iy in self.bins_range(axis=1, overflow=overflow)]
                    for ix in self.bins_range(axis=0, overflow=overflow)]
        ix = ix % self.nbins(axis=0, overflow=True)
        iy = iy % self.nbins(axis=1, overflow=True)
        return (self.GetBinError(ix, iy),
                self.GetBinError(ix, iy))

    def ravel(self, name=None):
        """
        Convert 2D histogram into 1D histogram with the y-axis repeated along
        the x-axis, similar to NumPy's ravel().
        """
        nbinsx = self.nbins(0)
        nbinsy = self.nbins(1)
        left_edge = self.xedgesl(1)
        right_edge = self.xedgesh(nbinsx)
        out = Hist(nbinsx * nbinsy,
                   left_edge, nbinsy * (right_edge - left_edge) + left_edge,
                   type=self.TYPE,
                   name=name,
                   title=self.title,
                   **self.decorators)
        for i, bin in enumerate(self.bins(overflow=False)):
            out.SetBinContent(i + 1, bin.value)
            out.SetBinError(i + 1, bin.error)
        return out

    def integral(self,
                 xbin1=None, xbin2=None,
                 ybin1=None, ybin2=None,
                 width=False,
                 error=False,
                 overflow=False):
        """
        Compute the integral and error over a range of bins
        """
        if xbin1 is None:
            xbin1 = 0 if overflow else 1
        if xbin2 is None:
            xbin2 = -1 if overflow else -2
        if ybin1 is None:
            ybin1 = 0 if overflow else 1
        if ybin2 is None:
            ybin2 = -1 if overflow else -2
        nbinsx = self.nbins(axis=0, overflow=True)
        xbin1 %= nbinsx
        xbin2 %= nbinsx
        nbinsy = self.nbins(axis=1, overflow=True)
        ybin1 %= nbinsy
        ybin2 %= nbinsy
        options = 'width' if width else ''
        if error:
            error = ROOT.Double()
            integral = super(_Hist2D, self).IntegralAndError(
                xbin1, xbin2, ybin1, ybin2, error, options)
            return integral, error
        return super(_Hist2D, self).Integral(
            xbin1, xbin2, ybin1, ybin2, options)


class _Hist3D(_HistBase):

    DIM = 3

    def x(self, index=None, overflow=False):
        return self._centers(0, index, overflow=overflow)

    def xerravg(self, index=None, overflow=False):
        return self._erravg(0, index, overflow=overflow)

    def xerrl(self, index=None, overflow=False):
        return self._erravg(0, index, overflow=overflow)

    def xerrh(self, index=None, overflow=False):
        return self._erravg(0, index, overflow=overflow)

    def xerr(self, index=None, overflow=False):
        return self._err(0, index, overflow=overflow)

    def xwidth(self, index=None, overflow=False):
        return self._width(0, index, overflow=overflow)

    def xedgesl(self, index=None, overflow=False):
        return self._edgesl(0, index, overflow=overflow)

    def xedgesh(self, index=None, overflow=False):
        return self._edgesh(0, index, overflow=overflow)

    def xedges(self, index=None, overflow=False):
        return self._edges(0, index, overflow=overflow)

    def y(self, index=None, overflow=False):
        return self._centers(1, index, overflow=overflow)

    def yerravg(self, index=None, overflow=False):
        return self._erravg(1, index, overflow=overflow)

    def yerrl(self, index=None, overflow=False):
        return self._erravg(1, index, overflow=overflow)

    def yerrh(self, index=None, overflow=False):
        return self._erravg(1, index, overflow=overflow)

    def yerr(self, index=None, overflow=False):
        return self._err(1, index, overflow=overflow)

    def ywidth(self, index=None, overflow=False):
        return self._width(1, index, overflow=overflow)

    def yedgesl(self, index=None, overflow=False):
        return self._edgesl(1, index, overflow=overflow)

    def yedgesh(self, index=None, overflow=False):
        return self._edgesh(1, index, overflow=overflow)

    def yedges(self, index=None, overflow=False):
        return self._edges(1, index, overflow=overflow)

    def z(self, index=None, overflow=False):
        return self._centers(2, index, overflow=overflow)

    def zerravg(self, index=None, overflow=False):
        return self._erravg(2, index, overflow=overflow)

    def zerrl(self, index=None, overflow=False):
        return self._erravg(2, index, overflow=overflow)

    def zerrh(self, index=None, overflow=False):
        return self._erravg(2, index, overflow=overflow)

    def zerr(self, index=None, overflow=False):
        return self._err(2, index, overflow=overflow)

    def zwidth(self, index=None, overflow=False):
        return self._width(2, index, overflow=overflow)

    def zedgesl(self, index=None, overflow=False):
        return self._edgesl(2, index, overflow=overflow)

    def zedgesh(self, index=None, overflow=False):
        return self._edgesh(2, index, overflow=overflow)

    def zedges(self, index=None, overflow=False):
        return self._edges(2, index, overflow=overflow)

    def werrh(self, index=None, overflow=False):
        return self.werravg(index, overflow=overflow)

    def werrl(self, index=None, overflow=False):
        return self.werravg(index, overflow=overflow)

    def w(self, ix=None, iy=None, iz=None, overflow=False):
        if ix is None and iy is None and iz is None:
            return [[[self.GetBinContent(ix, iy, iz)
                    for iz in self.bins_range(axis=2, overflow=overflow)]
                    for iy in self.bins_range(axis=1, overflow=overflow)]
                    for ix in self.bins_range(axis=0, overflow=overflow)]
        ix = ix % self.nbins(axis=0, overflow=True)
        iy = iy % self.nbins(axis=1, overflow=True)
        iz = iz % self.nbins(axis=2, overflow=True)
        return self.GetBinContent(ix, iy, iz)

    def werravg(self, ix=None, iy=None, iz=None, overflow=False):
        if ix is None and iy is None and iz is None:
            return [[[self.GetBinError(ix, iy, iz)
                    for iz in self.bins_range(axis=2, overflow=overflow)]
                    for iy in self.bins_range(axis=1, overflow=overflow)]
                    for ix in self.bins_range(axis=0, overflow=overflow)]
        ix = ix % self.nbins(axis=0, overflow=True)
        iy = iy % self.nbins(axis=1, overflow=True)
        iz = iz % self.nbins(axis=2, overflow=True)
        return self.GetBinError(ix, iy, iz)

    def werr(self, ix=None, iy=None, iz=None, overflow=False):
        if ix is None and iy is None and iz is None:
            return [[[
                (self.GetBinError(ix, iy, iz), self.GetBinError(ix, iy, iz))
                for iz in self.bins_range(axis=2, overflow=overflow)]
                for iy in self.bins_range(axis=1, overflow=overflow)]
                for ix in self.bins_range(axis=0, overflow=overflow)]
        ix = ix % self.nbins(axis=0, overflow=True)
        iy = iy % self.nbins(axis=1, overflow=True)
        iz = iz % self.nbins(axis=2, overflow=True)
        return (self.GetBinError(ix, iy, iz),
                self.GetBinError(ix, iy, iz))

    def integral(self,
                 xbin1=1, xbin2=-2,
                 ybin1=1, ybin2=-2,
                 zbin1=1, zbin2=-2,
                 width=False,
                 error=False,
                 overflow=False):
        """
        Compute the integral and error over a range of bins
        """
        if xbin1 is None:
            xbin1 = 0 if overflow else 1
        if xbin2 is None:
            xbin2 = -1 if overflow else -2
        if ybin1 is None:
            ybin1 = 0 if overflow else 1
        if ybin2 is None:
            ybin2 = -1 if overflow else -2
        if zbin1 is None:
            zbin1 = 0 if overflow else 1
        if zbin2 is None:
            zbin2 = -1 if overflow else -2
        nbinsx = self.nbins(axis=0, overflow=True)
        xbin1 %= nbinsx
        xbin2 %= nbinsx
        nbinsy = self.nbins(axis=1, overflow=True)
        ybin1 %= nbinsy
        ybin2 %= nbinsy
        nbinsz = self.nbins(axis=2, overflow=True)
        zbin1 %= nbinsz
        zbin2 %= nbinsz
        options = 'width' if width else ''
        if error:
            error = ROOT.Double()
            integral = super(_Hist3D, self).IntegralAndError(
                xbin1, xbin2, ybin1, ybin2, zbin1, zbin2, error, options)
            return integral, error
        return super(_Hist3D, self).Integral(
            xbin1, xbin2, ybin1, ybin2, zbin1, zbin2, options)


def _Hist_class(type='F'):
    type = type.upper()
    if type not in _HistBase.TYPES:
        raise TypeError(
            "No histogram available with bin type {0}".format(type))
    rootclass = _HistBase.TYPES[type][0]

    class Hist(_Hist, rootclass):
        _ROOT = rootclass
        TYPE = type

        def __init__(self, *args, **kwargs):
            params = self._parse_args(args)
            name = kwargs.pop('name', None)
            title = kwargs.pop('title', None)
            if params[0]['bins'] is None:
                super(Hist, self).__init__(
                    params[0]['nbins'], params[0]['low'], params[0]['high'],
                    name=name, title=title)
            else:
                super(Hist, self).__init__(
                    params[0]['nbins'], array('d', params[0]['bins']),
                    name=name, title=title)
            self._post_init(**kwargs)

    return Hist


def _Hist2D_class(type='F'):
    type = type.upper()
    if type not in _HistBase.TYPES:
        raise TypeError(
            "No histogram available with bin type {0}".format(type))
    rootclass = _HistBase.TYPES[type][1]

    class Hist2D(_Hist2D, rootclass):
        _ROOT = rootclass
        TYPE = type

        def __init__(self, *args, **kwargs):
            params = self._parse_args(args)
            name = kwargs.pop('name', None)
            title = kwargs.pop('title', None)
            if params[0]['bins'] is None and params[1]['bins'] is None:
                super(Hist2D, self).__init__(
                    params[0]['nbins'], params[0]['low'], params[0]['high'],
                    params[1]['nbins'], params[1]['low'], params[1]['high'],
                    name=name, title=title)
            elif params[0]['bins'] is None and params[1]['bins'] is not None:
                super(Hist2D, self).__init__(
                    params[0]['nbins'], params[0]['low'], params[0]['high'],
                    params[1]['nbins'], array('d', params[1]['bins']),
                    name=name, title=title)
            elif params[0]['bins'] is not None and params[1]['bins'] is None:
                super(Hist2D, self).__init__(
                    params[0]['nbins'], array('d', params[0]['bins']),
                    params[1]['nbins'], params[1]['low'], params[1]['high'],
                    name=name, title=title)
            else:
                super(Hist2D, self).__init__(
                    params[0]['nbins'], array('d', params[0]['bins']),
                    params[1]['nbins'], array('d', params[1]['bins']),
                    name=name, title=title)
            self._post_init(**kwargs)

    return Hist2D


def _Hist3D_class(type='F'):
    type = type.upper()
    if type not in _HistBase.TYPES:
        raise TypeError(
            "No histogram available with bin type {0}".format(type))
    rootclass = _HistBase.TYPES[type][2]

    class Hist3D(_Hist3D, rootclass):
        _ROOT = rootclass
        TYPE = type

        def __init__(self, *args, **kwargs):
            params = self._parse_args(args)
            name = kwargs.pop('name', None)
            title = kwargs.pop('title', None)
            # ROOT is missing constructors for TH3...
            if (params[0]['bins'] is None and
                    params[1]['bins'] is None and
                    params[2]['bins'] is None):
                super(Hist3D, self).__init__(
                    params[0]['nbins'], params[0]['low'], params[0]['high'],
                    params[1]['nbins'], params[1]['low'], params[1]['high'],
                    params[2]['nbins'], params[2]['low'], params[2]['high'],
                    name=name, title=title)
            else:
                if params[0]['bins'] is None:
                    step = ((params[0]['high'] - params[0]['low'])
                            / float(params[0]['nbins']))
                    params[0]['bins'] = [
                        params[0]['low'] + n * step
                        for n in range(params[0]['nbins'] + 1)]
                if params[1]['bins'] is None:
                    step = ((params[1]['high'] - params[1]['low'])
                            / float(params[1]['nbins']))
                    params[1]['bins'] = [
                        params[1]['low'] + n * step
                        for n in range(params[1]['nbins'] + 1)]
                if params[2]['bins'] is None:
                    step = ((params[2]['high'] - params[2]['low'])
                            / float(params[2]['nbins']))
                    params[2]['bins'] = [
                        params[2]['low'] + n * step
                        for n in range(params[2]['nbins'] + 1)]
                super(Hist3D, self).__init__(
                    params[0]['nbins'], array('d', params[0]['bins']),
                    params[1]['nbins'], array('d', params[1]['bins']),
                    params[2]['nbins'], array('d', params[2]['bins']),
                    name=name, title=title)
            self._post_init(**kwargs)

    return Hist3D


_HIST_CLASSES_1D = {}
_HIST_CLASSES_2D = {}
_HIST_CLASSES_3D = {}

for bintype in _HistBase.TYPES.keys():
    cls = _Hist_class(type=bintype)
    snake_case_methods(cls)
    _HIST_CLASSES_1D[bintype] = cls

    cls = _Hist2D_class(type=bintype)
    snake_case_methods(cls)
    _HIST_CLASSES_2D[bintype] = cls

    cls = _Hist3D_class(type=bintype)
    snake_case_methods(cls)
    _HIST_CLASSES_3D[bintype] = cls


[docs]class Hist(_Hist, QROOT.TH1): """ Returns a 1-dimensional Hist object which inherits from the associated ROOT.TH1* class (where * is C, S, I, F, or D depending on the type keyword argument) """ _ROOT = QROOT.TH1 @classmethod def dynamic_cls(cls, type='F'): return _HIST_CLASSES_1D[type] def __new__(cls, *args, **kwargs): if len(args) == 1: other = args[0] kwargs.setdefault('type', 'F') if isinstance(other, HistView): obj = Hist(other.xedges, **kwargs) obj.fill_view(other.hist[:]) obj.entries = other.hist.entries return obj elif isinstance(other, _Hist): obj = other.empty_clone(**kwargs) obj[:] = other[:] obj.entries = other.entries return obj elif isinstance(other, _Graph1DBase): # attempt to convert graph to histogram if len(other) == 0: raise ValueError("cannot construct a histogram " "from an empty graph") edges = [other.x(0) - other.xerrl(0)] # first edge values = [] errors = [] for ipoint in range(len(other)): edges.append(other.x(ipoint) + other.xerrh(ipoint)) values.append(other.y(ipoint)) errors.append(max(abs(other.yerrh(ipoint)), abs(other.yerrl(ipoint)))) obj = Hist(edges, **kwargs) for idx, (y, yerr) in enumerate(zip(values, errors)): obj[idx + 1] = (y, yerr) return obj type = kwargs.pop('type', 'F').upper() return cls.dynamic_cls(type)(*args, **kwargs)
# alias Hist1D -> Hist Hist1D = Hist
[docs]class Hist2D(_Hist2D, QROOT.TH2): """ Returns a 2-dimensional Hist object which inherits from the associated ROOT.TH1* class (where * is C, S, I, F, or D depending on the type keyword argument) """ _ROOT = QROOT.TH2 @classmethod def dynamic_cls(cls, type='F'): return _HIST_CLASSES_2D[type] def __new__(cls, *args, **kwargs): if len(args) == 1: other = args[0] kwargs.setdefault('type', 'F') if isinstance(other, Hist2DView): obj = Hist2D(other.xedges, other.yedges, **kwargs) obj.fill_view(other.hist[:,:]) obj.entries = other.hist.entries return obj elif isinstance(other, _Hist2D): obj = other.empty_clone(**kwargs) obj[:] = other[:] obj.entries = other.entries return obj type = kwargs.pop('type', 'F').upper() return cls.dynamic_cls(type)(*args, **kwargs)
[docs]class Hist3D(_Hist3D, QROOT.TH3): """ Returns a 3-dimensional Hist object which inherits from the associated ROOT.TH1* class (where * is C, S, I, F, or D depending on the type keyword argument) """ _ROOT = QROOT.TH3 @classmethod def dynamic_cls(cls, type='F'): return _HIST_CLASSES_3D[type] def __new__(cls, *args, **kwargs): if len(args) == 1: other = args[0] kwargs.setdefault('type', 'F') if isinstance(other, Hist3DView): obj = Hist3D(other.xedges, other.yedges, other.zedges, **kwargs) obj.fill_view(other.hist[:,:,:]) obj.entries = other.hist.entries return obj elif isinstance(other, _Hist3D): obj = other.empty_clone(**kwargs) obj[:] = other[:] obj.entries = other.entries return obj type = kwargs.pop('type', 'F').upper() return cls.dynamic_cls(type)( *args, **kwargs)
[docs]class HistStack(Plottable, NamedObject, QROOT.THStack): _ROOT = QROOT.THStack def __init__(self, hists=None, name=None, title=None, stacked=True, **kwargs): super(HistStack, self).__init__(name=name, title=title) self._post_init(hists=hists, stacked=stacked, **kwargs) def _post_init(self, hists=None, stacked=True, **kwargs): super(HistStack, self)._post_init(**kwargs) self.hists = [] self.dim = 1 current_hists = super(HistStack, self).GetHists() if current_hists: for i, hist in enumerate(current_hists): hist = asrootpy(hist) if i == 0: self.dim = dim(hist) elif dim(hist) != self.dim: raise TypeError( "Dimensions of the contained histograms are not equal") self.hists.append(hist) self.stacked = stacked if stacked: # histogram binning must be identical self.sum = sum(self.hists) if self.hists else None if hists: for h in hists: self.Add(h) def __dim__(self): return self.dim def GetHists(self): return [hist for hist in self.hists] def Add(self, hist): if isinstance(hist, _Hist) or isinstance(hist, _Hist2D): if not self: self.dim = dim(hist) if self.stacked: self.sum = hist.Clone() elif dim(self) != dim(hist): raise TypeError( "Dimension of histogram does not match dimension " "of already contained histograms") elif self.stacked: self.sum += hist self.hists.append(hist) super(HistStack, self).Add(hist, hist.drawstyle) else: raise TypeError( "Only 1D and 2D histograms are supported") def __add__(self, other): if not isinstance(other, HistStack): raise TypeError( "Addition not supported for HistStack and {0}".format( other.__class__.__name__)) clone = HistStack() for hist in self: clone.Add(hist) for hist in other: clone.Add(hist) return clone def __iadd__(self, other): if not isinstance(other, HistStack): raise TypeError( "Addition not supported for HistStack and {0}".format( other.__class__.__name__)) for hist in other: self.Add(hist) return self def __len__(self): return len(self.GetHists()) def __getitem__(self, index): return self.GetHists()[index] def __iter__(self): for hist in self.hists: yield hist def __nonzero__(self): return len(self) != 0 __bool__ = __nonzero__ def Scale(self, value): for hist in self: hist.Scale(value) def Integral(self, start=None, end=None): integral = 0 if start is not None and end is not None: for hist in self: integral += hist.Integral(start, end) else: for hist in self: integral += hist.Integral() return integral def lowerbound(self, axis=0): if not self: return None # negative infinity return min(hist.lowerbound(axis=axis) for hist in self) def upperbound(self, axis=0): if not self: return () # positive infinity return max(hist.upperbound(axis=axis) for hist in self) def max(self, include_error=False): if not self: return 0 if self.stacked: return self.sum.max(include_error=include_error) return max([hist.max(include_error=include_error) for hist in self.hists]) def min(self, include_error=False): if not self: return 0 if self.stacked: return self.sum.min(include_error=include_error) return min([hist.min(include_error=include_error) for hist in self.hists]) def Clone(self, name=None): clone = HistStack(name=name, title=self.GetTitle(), stacked=self.stacked, **self.decorators) for hist in self: clone.Add(hist.Clone()) return clone def GetHistogram(self): return asrootpy(super(HistStack, self).GetHistogram()) def GetZaxis(self): # ROOT is missing this method... return self.GetHistogram().zaxis
@snake_case_methods class Efficiency(Plottable, NamelessConstructorObject, QROOT.TEfficiency): _ROOT = QROOT.TEfficiency def __init__(self, passed, total, name=None, title=None, **kwargs): super(Efficiency, self).__init__(passed, total, name=name, title=title) self._post_init(**kwargs) @property def passed(self): return asrootpy(self.GetPassedHistogram()) @passed.setter def passed(self, hist): self.SetPassedHistogram(hist, 'f') @property def total(self): return asrootpy(self.GetTotalHistogram()) @total.setter def total(self, hist): self.SetTotalHistogram(hist, 'f') def __len__(self): return len(self.total) def __getitem__(self, idx): return self.GetEfficiency(idx) def __add__(self, other): copy = self.Clone() copy.Add(other) return copy def __iadd__(self, other): super(Efficiency, self).Add(other) return self def __iter__(self): for idx in range(len(self)): yield self.GetEfficiency(idx) def efficiencies(self, overflow=False): if overflow: start = 0 end = len(self) else: start = 1 end = len(self) - 1 for idx in range(start, end): yield self.GetEfficiency(idx) def errors(self, overflow=False): if overflow: start = 0 end = len(self) else: start = 1 end = len(self) - 1 for idx in range(start, end): yield ( self.GetEfficiencyErrorLow(idx), self.GetEfficiencyErrorUp(idx)) def overall_efficiency(self, overflow=False): if self.total.Integral() == 0: return 0 nbins = self.passed.nbins() if overflow: bins_to_merge = (0, nbins+1) else: bins_to_merge = (1, nbins) hpass = self.passed.merge_bins([bins_to_merge]) htot = self.total.merge_bins([bins_to_merge]) tot_eff = Efficiency(hpass, htot) return (tot_eff.GetEfficiency(1), tot_eff.GetEfficiencyErrorLow(1), tot_eff.GetEfficiencyErrorUp(1)) @property def graph(self): """ Create and return the graph for a 1D TEfficiency """ return asrootpy(self.CreateGraph()) @property def painted_graph(self): """ Returns the painted graph for a 1D TEfficiency, or if it isn't available, generates one on an `invisible_canvas`. """ if not self.GetPaintedGraph(): with invisible_canvas(): self.Draw() assert self.GetPaintedGraph(), ( "Failed to create TEfficiency::GetPaintedGraph") the_graph = asrootpy(self.GetPaintedGraph()) # Ensure it has the same style as this one. the_graph.decorate(**self.decorators) return the_graph @property def histogram(self): """ Create and return the histogram for a 2D TEfficiency """ return asrootpy(self.CreateHistogram()) @property def painted_histogram(self): """ Returns the painted histogram for a 2D TEfficiency, or if it isn't available, generates one on an `invisible_canvas`. """ if not self.GetPaintedHistogram(): with invisible_canvas(): self.Draw() assert self.GetPaintedHistogram(), ( "Failed to create TEfficiency::GetPaintedHistogram") the_hist = asrootpy(self.GetPaintedHistogram()) # Ensure it has the same style as this one. the_hist.decorate(**self.decorators) return the_hist def histogram(data, *args, **kwargs): """ Create and fill a one-dimensional histogram. The same arguments as the ``Hist`` class are expected. If the number of bins and the ranges are not specified they are automatically deduced with the ``autobinning`` function using the method specified by the ``binning`` argument. Only one-dimensional histogramming is supported. """ from .autobinning import autobinning dim = kwargs.pop('dim', 1) if dim != 1: raise NotImplementedError if 'binning' in kwargs: args = autobinning(data, kwargs['binning']) del kwargs['binning'] histo = Hist(*args, **kwargs) for d in data: histo.Fill(d) return list(histo.xedgesl()), histo