From eda6350283a33155c2f6bd90a8d7c72db62d0d7d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Jun 2018 12:40:15 +0100 Subject: [PATCH] Histogram enhancements (#2812) --- holoviews/core/element.py | 4 ++-- holoviews/core/spaces.py | 5 ++--- holoviews/operation/element.py | 25 +++++++++++++++++++------ tests/operation/testoperation.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/holoviews/core/element.py b/holoviews/core/element.py index a0fef729db..3c8f0280b6 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -23,7 +23,7 @@ class Element(ViewableElement, Composable, Overlayable): group = param.String(default='Element', constant=True) def hist(self, dimension=None, num_bins=20, bin_range=None, - adjoin=True, individually=True, **kwargs): + adjoin=True, **kwargs): """ The hist method generates a histogram to be adjoined to the Element in an AdjointLayout. By default the histogram is @@ -37,7 +37,7 @@ def hist(self, dimension=None, num_bins=20, bin_range=None, hists = [] for d in dimension[::-1]: hist = histogram(self, num_bins=num_bins, bin_range=bin_range, - individually=individually, dimension=d, **kwargs) + dimension=d, **kwargs) hists.append(hist) if adjoin: layout = self diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index c85a213ecb..928ab2e784 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -421,8 +421,7 @@ def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kw if issubclass(self.type, (NdOverlay, Overlay)) and 'index' not in kwargs: kwargs['index'] = 0 for k, v in self.data.items(): - hists = v.hist(adjoin=False, bin_range=bin_range, - individually=individually, num_bins=num_bins, + hists = v.hist(adjoin=False, bin_range=bin_range, num_bins=num_bins, style_prefix=style_prefix, **kwargs) if isinstance(hists, Layout): for i, hist in enumerate(hists): @@ -1421,7 +1420,7 @@ def overlay(self, dimensions=None, **kwargs): return self.groupby(dims, group_type=NdOverlay) - def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kwargs): + def hist(self, num_bins=20, bin_range=None, adjoin=True, **kwargs): """ Computes a histogram from the object and adjoins it by default. By default the histogram is computed for the bottom diff --git a/holoviews/operation/element.py b/holoviews/operation/element.py index 07cdf83073..41dba47b4b 100644 --- a/holoviews/operation/element.py +++ b/holoviews/operation/element.py @@ -486,6 +486,12 @@ class histogram(Operation): bin_range = param.NumericTuple(default=None, length=2, doc=""" Specifies the range within which to compute the bins.""") + bins = param.ClassSelector(default=None, class_=(np.ndarray, list, tuple), doc=""" + An explicit set of bin edges.""") + + cumulative = param.Boolean(default=False, doc=""" + Whether to compute the cumulative histogram""") + dimension = param.String(default=None, doc=""" Along which dimension of the Element to compute the histogram.""") @@ -495,9 +501,6 @@ class histogram(Operation): groupby = param.ClassSelector(default=None, class_=(basestring, Dimension), doc=""" Defines a dimension to group the Histogram returning an NdOverlay of Histograms.""") - individually = param.Boolean(default=True, doc=""" - Specifies whether the histogram will be rescaled for each Element in a UniformNdMapping.""") - log = param.Boolean(default=False, doc=""" Whether to use base 10 logarithmic samples for the bin edges.""") @@ -555,15 +558,21 @@ def _process(self, view, key=None): hist_range = (0, 1) datetimes = False + bins = None if self.p.bins is None else np.asarray(self.p.bins) steps = self.p.num_bins + 1 start, end = hist_range if data.dtype.kind == 'M' or (data.dtype.kind == 'O' and isinstance(data[0], datetime_types)): start, end = dt_to_int(start, 'ns'), dt_to_int(end, 'ns') datetimes = True data = data.astype('datetime64[ns]').astype('int64') * 1000. - hist_range = start, end + if bins is not None: + bins = bins.astype('datetime64[ns]').astype('int64') * 1000. + else: + hist_range = start, end - if self.p.log: + if self.p.bins: + edges = bins + elif self.p.log: bin_min = max([abs(start), data[data>0].min()]) edges = np.logspace(np.log10(bin_min), np.log10(end), steps) else: @@ -601,7 +610,11 @@ def _process(self, view, key=None): if view.group != view.__class__.__name__: params['group'] = view.group - return Histogram((hist, edges), kdims=[view.get_dimension(selected_dim)], + if self.p.cumulative: + hist = np.cumsum(hist) + if self.p.normed in (True, 'integral'): + hist *= edges[1]-edges[0] + return Histogram((edges, hist), kdims=[view.get_dimension(selected_dim)], label=view.label, **params) diff --git a/tests/operation/testoperation.py b/tests/operation/testoperation.py index e1d709fec5..213d68bb5e 100644 --- a/tests/operation/testoperation.py +++ b/tests/operation/testoperation.py @@ -127,6 +127,35 @@ def test_points_histogram_bin_range(self): hist = Histogram(([0.25, 0.25, 0.5], [0., 1., 2., 3.])) self.assertEqual(op_hist, hist) + def test_points_histogram_explicit_bins(self): + points = Points([float(i) for i in range(10)]) + op_hist = histogram(points, bins=[0, 1, 3], normed=False) + + # Make sure that the name and label are as desired + op_freq_dim = op_hist.get_dimension('x_frequency') + self.assertEqual(op_freq_dim.label, 'x Frequency') + + # Because the operation labels are now different from the + # default Element label, change back before comparing. + op_hist = op_hist.redim(x_frequency='Frequency') + hist = Histogram(([0, 1, 3], [1, 3])) + self.assertEqual(op_hist, hist) + + def test_points_histogram_cumulative(self): + arr = np.arange(4) + points = Points(arr) + op_hist = histogram(points, cumulative=True, num_bins=3, normed=False) + + # Make sure that the name and label are as desired + op_freq_dim = op_hist.get_dimension('x_frequency') + self.assertEqual(op_freq_dim.label, 'x Frequency') + + # Because the operation labels are now different from the + # default Element label, change back before comparing. + op_hist = op_hist.redim(x_frequency='Frequency') + hist = Histogram(([0, 1, 2, 3], [1, 2, 4])) + self.assertEqual(op_hist, hist) + def test_points_histogram_not_normed(self): points = Points([float(i) for i in range(10)]) op_hist = histogram(points, num_bins=3, normed=False)