Skip to content

Commit

Permalink
Add a isfinite utility to use throughout code (#2715)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored and jlstevens committed May 22, 2018
1 parent 8ce2f52 commit 8711663
Show file tree
Hide file tree
Showing 14 changed files with 88 additions and 38 deletions.
2 changes: 1 addition & 1 deletion holoviews/core/data/__init__.py
Expand Up @@ -249,7 +249,7 @@ def range(self, dim, data_range=True):
dim = self.get_dimension(dim)
if dim is None:
return (None, None)
elif all(v is not None and np.isfinite(v) for v in dim.range):
elif all(util.isfinite(v) for v in dim.range):
return dim.range
elif dim in self.dimensions() and data_range and len(self):
lower, upper = self.interface.range(self, dim)
Expand Down
4 changes: 2 additions & 2 deletions holoviews/core/dimension.py
Expand Up @@ -11,7 +11,7 @@
import numpy as np
import param

from ..core.util import (basestring, sanitize_identifier,
from ..core.util import (basestring, sanitize_identifier, isfinite,
group_sanitizer, label_sanitizer, max_range,
find_range, dimension_sanitizer, OrderedDict,
bytes_to_unicode, unicode, dt64_to_dt, unique_array,
Expand Down Expand Up @@ -1081,7 +1081,7 @@ def range(self, dimension, data_range=True):
dimension = self.get_dimension(dimension)
if dimension is None:
return (None, None)
elif all(v is not None and np.isfinite(v) for v in dimension.range):
elif all(isfinite(v) for v in dimension.range):
return dimension.range
elif data_range:
if dimension in self.kdims+self.vdims:
Expand Down
22 changes: 19 additions & 3 deletions holoviews/core/util.py
Expand Up @@ -46,7 +46,7 @@
from pandas.core.dtypes.dtypes import DatetimeTZDtypeType
else:
from pandas.types.dtypes import DatetimeTZDtypeType
datetime_types = datetime_types + (pd.Timestamp, DatetimeTZDtypeType)
datetime_types = datetime_types + (pd.Timestamp, DatetimeTZDtypeType, pd.Period)
timedelta_types = timedelta_types + (pd.Timedelta,)
except ImportError:
pd = None
Expand Down Expand Up @@ -692,6 +692,22 @@ def isnumeric(val):
return False


def isfinite(val):
"""
Helper function to determine if scalar or array value is finite extending
np.isfinite with support for None, string, datetime types.
"""
if val is None:
return False
elif isinstance(val, np.ndarray):
if val.dtype.kind in 'USMO':
return np.ones_like(val, dtype=bool)
return np.isfinite(val)
elif isinstance(val, datetime_types+timedelta_types+(basestring,)):
return True
return np.isfinite(val)


def find_minmax(lims, olims):
"""
Takes (a1, a2) and (b1, b2) as input and returns
Expand Down Expand Up @@ -756,8 +772,8 @@ def dimension_range(lower, upper, dimension):
"""
lower, upper = max_range([(lower, upper), dimension.soft_range])
dmin, dmax = dimension.range
lower = lower if dmin is None or not np.isfinite(dmin) else dmin
upper = upper if dmax is None or not np.isfinite(dmax) else dmax
lower = dmin if isfinite(dmin) else lower
upper = dmax if isfinite(dmax) else upper
return lower, upper


Expand Down
2 changes: 1 addition & 1 deletion holoviews/element/raster.py
Expand Up @@ -323,7 +323,7 @@ def _validate(self, data_bounds, supplied_bounds):
bounds = data_bounds

if not all(np.isclose(r, c, rtol=self.rtol) for r, c in zip(bounds, self.bounds.lbrt())
if not isinstance(r, util.datetime_types) and np.isfinite(r)):
if util.isfinite(r) and not isinstance(r, util.datetime_types)):
raise ValueError('Supplied Image bounds do not match the coordinates defined '
'in the data. Bounds only have to be declared if no coordinates '
'are supplied, otherwise they must match the data. To change '
Expand Down
6 changes: 3 additions & 3 deletions holoviews/operation/element.py
Expand Up @@ -13,7 +13,7 @@
HoloMap, Dataset, Element, Collator, Dimension)
from ..core.data import ArrayInterface, DictInterface
from ..core.util import (group_sanitizer, label_sanitizer, pd,
basestring, datetime_types)
basestring, datetime_types, isfinite)
from ..element.chart import Histogram, Scatter
from ..element.raster import Raster, Image, RGB, QuadMesh
from ..element.path import Contours, Polygons
Expand Down Expand Up @@ -548,10 +548,10 @@ def _process(self, view, key=None):
else:
weights = None

data = data[np.isfinite(data)]
data = data[isfinite(data)]
hist_range = self.p.bin_range or view.range(selected_dim)
# Avoids range issues including zero bin range and empty bins
if hist_range == (0, 0) or any(not np.isfinite(r) for r in hist_range):
if hist_range == (0, 0) or any(not isfinite(r) for r in hist_range):
hist_range = (0, 1)
if self.p.log:
bin_min = max([abs(hist_range[0]), data[data>0].min()])
Expand Down
14 changes: 7 additions & 7 deletions holoviews/operation/stats.py
Expand Up @@ -3,7 +3,7 @@

from ..core import Dimension, Dataset, NdOverlay
from ..core.operation import Operation
from ..core.util import basestring, cartesian_product
from ..core.util import basestring, cartesian_product, isfinite
from ..element import (Curve, Area, Image, Distribution, Bivariate,
Contours, Polygons)

Expand All @@ -13,9 +13,9 @@
def _kde_support(bin_range, bw, gridsize, cut, clip):
"""Establish support for a kernel density estimate."""
kmin, kmax = bin_range[0] - bw * cut, bin_range[1] + bw * cut
if clip[0] is not None and np.isfinite(clip[0]):
if isfinite(clip[0]):
kmin = max(kmin, clip[0])
if clip[1] is not None and np.isfinite(clip[1]):
if isfinite(clip[1]):
kmax = min(kmax, clip[1])
return np.linspace(kmin, kmax, gridsize)

Expand Down Expand Up @@ -95,7 +95,7 @@ def _process(self, element, key=None):

data = element.dimension_values(selected_dim)
bin_range = self.p.bin_range or element.range(selected_dim)
if bin_range == (0, 0) or any(not np.isfinite(r) for r in bin_range):
if bin_range == (0, 0) or any(not isfinite(r) for r in bin_range):
bin_range = (0, 1)
elif bin_range[0] == bin_range[1]:
bin_range = (bin_range[0]-0.5, bin_range[1]+0.5)
Expand Down Expand Up @@ -186,16 +186,16 @@ def _process(self, element, key=None):
data = element.array([0, 1]).T
xmin, xmax = self.p.x_range or element.range(0)
ymin, ymax = self.p.y_range or element.range(1)
if any(not np.isfinite(v) for v in (xmin, xmax)):
if any(not isfinite(v) for v in (xmin, xmax)):
xmin, xmax = -0.5, 0.5
elif xmin == xmax:
xmin, xmax = xmin-0.5, xmax+0.5
if any(not np.isfinite(v) for v in (ymin, ymax)):
if any(not isfinite(v) for v in (ymin, ymax)):
ymin, ymax = -0.5, 0.5
elif ymin == ymax:
ymin, ymax = ymin-0.5, ymax+0.5

data = data[:, np.isfinite(data).min(axis=0)] if data.shape[1] > 1 else np.empty((2, 0))
data = data[:, isfinite(data).min(axis=0)] if data.shape[1] > 1 else np.empty((2, 0))
if data.shape[1] > 1:
kde = stats.gaussian_kde(data)
if self.p.bandwidth:
Expand Down
6 changes: 3 additions & 3 deletions holoviews/plotting/bokeh/chart.py
Expand Up @@ -7,7 +7,7 @@
from bokeh.transform import jitter

from ...core import Dataset, OrderedDict
from ...core.util import max_range, basestring, dimension_sanitizer
from ...core.util import max_range, basestring, dimension_sanitizer, isfinite
from ...element import Bars
from ...operation import interpolate_curve
from ..util import compute_sizes, get_min_distance, dim_axis_label
Expand Down Expand Up @@ -344,8 +344,8 @@ def get_data(self, element, ranges, style):
def get_extents(self, element, ranges):
x0, y0, x1, y1 = super(HistogramPlot, self).get_extents(element, ranges)
ylow, yhigh = element.get_dimension(1).range
y0 = np.nanmin([0, y0]) if ylow is None or not np.isfinite(ylow) else ylow
y1 = np.nanmax([0, y1]) if yhigh is None or not np.isfinite(yhigh) else yhigh
y0 = ylow if isfinite(ylow) else np.nanmin([0, y0])
y1 = yhigh if isfinite(yhigh) else np.nanmax([0, y1])
return (x0, y0, x1, y1)


Expand Down
10 changes: 4 additions & 6 deletions holoviews/plotting/bokeh/element.py
Expand Up @@ -523,11 +523,9 @@ def _update_range(self, axis_range, low, high, factors, invert, shared, log, str
self.warning("Logarithmic axis range encountered value less than or equal to zero, "
"please supply explicit lower-bound to override default of %.3f." % low)
updates = {}
if low is not None and (isinstance(low, util.datetime_types)
or np.isfinite(low)):
if util.isfinite(low):
updates['start'] = (axis_range.start, low)
if high is not None and (isinstance(high, util.datetime_types)
or np.isfinite(high)):
if util.isfinite(high):
updates['end'] = (axis_range.end, high)
for k, (old, new) in updates.items():
axis_range.update(**{k:new})
Expand Down Expand Up @@ -1126,9 +1124,9 @@ def _get_cmapper_opts(self, low, high, factors, colors):
if isinstance(low, (bool, np.bool_)): low = int(low)
if isinstance(high, (bool, np.bool_)): high = int(high)
opts = {}
if np.isfinite(low):
if util.isfinite(low):
opts['low'] = low
if np.isfinite(high):
if util.isfinite(high):
opts['high'] = high
color_opts = [('NaN', 'nan_color'), ('max', 'high_color'), ('min', 'low_color')]
opts.update({opt: colors[name] for name, opt in color_opts if name in colors})
Expand Down
4 changes: 2 additions & 2 deletions holoviews/plotting/bokeh/hex_tiles.py
Expand Up @@ -10,7 +10,7 @@

from ...core import Dimension, Operation
from ...core.options import Compositor, SkipRendering
from ...core.util import basestring
from ...core.util import basestring, isfinite
from ...element import HexTiles
from .element import ColorbarPlot, line_properties, fill_properties
from .util import bokeh_version
Expand Down Expand Up @@ -53,7 +53,7 @@ def _process(self, element, key=None):
x, y = (element.dimension_values(i) for i in indexes)
if not len(x):
return element.clone([])
finite = np.isfinite(x) & np.isfinite(y)
finite = isfinite(x) & isfinite(y)
x, y = x[finite], y[finite]
q, r = cartesian_to_axial(x, y, size, orientation+'top', scale)
coords = q, r
Expand Down
2 changes: 1 addition & 1 deletion holoviews/plotting/bokeh/raster.py
Expand Up @@ -162,7 +162,7 @@ def get_data(self, element, ranges, style):
xc, yc = [], []
for xs, ys, zval in zip(X, Y, zvals):
xs, ys = xs[:-1], ys[:-1]
if np.isfinite(zval) and all(np.isfinite(c) for c in xs) and all(np.isfinite(c) for c in ys):
if np.isfinite(zval) and all(np.isfinite(xs)) and all(np.isfinite(ys)):
XS.append(list(xs))
YS.append(list(ys))
mask.append(True)
Expand Down
10 changes: 5 additions & 5 deletions holoviews/plotting/mpl/chart.py
Expand Up @@ -14,7 +14,7 @@
import param

from ...core import OrderedDict, Dimension, Store
from ...core.util import match_spec, unique_iterator, basestring, max_range
from ...core.util import match_spec, unique_iterator, basestring, max_range, isfinite
from ...element import Raster, HeatMap
from ...operation import interpolate_curve
from ..plot import PlotSelector
Expand Down Expand Up @@ -316,8 +316,8 @@ def _compute_ticks(self, element, edges, widths, lims):
def get_extents(self, element, ranges):
x0, y0, x1, y1 = super(HistogramPlot, self).get_extents(element, ranges)
ylow, yhigh = element.get_dimension(1).range
y0 = np.nanmin([0, y0]) if ylow is None or not np.isfinite(ylow) else ylow
y1 = np.nanmax([0, y1]) if yhigh is None or not np.isfinite(yhigh) else yhigh
y0 = ylow if isfinite(ylow) else np.nanmin([0, y0])
y1 = yhigh if isfinite(yhigh) else np.nanmax([0, y1])
return (x0, y0, x1, y1)


Expand Down Expand Up @@ -860,7 +860,7 @@ def _create_bars(self, axis, element):

# Update variables
bars[tuple(val_key)] = bar
prev += val if np.isfinite(val) else 0
prev += val if isfinite(val) else 0
labels.append(label)
title = [element.kdims[indices[cg]].pprint_label
for cg in self.color_by if indices[cg] < ndims]
Expand Down Expand Up @@ -890,7 +890,7 @@ def update_handles(self, key, axis, element, ranges, style):
height = float(vals[0]) if len(vals) else np.NaN
bar[0].set_height(height)
bar[0].set_y(prev)
prev += height if np.isfinite(height) else 0
prev += height if isfinite(height) else 0
return {'xticks': self.handles['xticks']}


Expand Down
5 changes: 2 additions & 3 deletions holoviews/plotting/plot.py
Expand Up @@ -18,7 +18,7 @@
from ..core.options import Store, Compositor, SkipRendering
from ..core.overlay import NdOverlay
from ..core.spaces import HoloMap, DynamicMap
from ..core.util import stream_parameters
from ..core.util import stream_parameters, isfinite
from ..element import Table
from .util import (get_dynamic_mode, initialize_unbounded, dim_axis_label,
attach_streams, traverse_setter, get_nested_streams,
Expand Down Expand Up @@ -799,8 +799,7 @@ def get_extents(self, view, ranges):
else:
max_extent = []
for l1, l2 in zip(range_extents, extents):
if (isinstance(l2, util.datetime_types)
or (l2 is not None and np.isfinite(l2))):
if isfinite(l2):
max_extent.append(l2)
else:
max_extent.append(l1)
Expand Down
6 changes: 6 additions & 0 deletions tests/core/data/testdataset.py
Expand Up @@ -5,6 +5,7 @@
from unittest import SkipTest
from nose.plugins.attrib import attr
from itertools import product
import datetime

import numpy as np
try:
Expand Down Expand Up @@ -468,6 +469,11 @@ def test_dataset_dict_init(self):
dataset = Dataset(dict(zip(self.xs, self.ys)), kdims=['A'], vdims=['B'])
self.assertTrue(isinstance(dataset.data, self.data_instance_type))

def test_dataset_range_with_dimension_range(self):
dt64 = np.array([np.datetime64(datetime.datetime(2017, 1, i)) for i in range(1, 4)])
ds = Dataset(dt64, [Dimension('Date', range=(dt64[0], dt64[-1]))])
self.assertEqual(ds.range('Date'), (dt64[0], dt64[-1]))

# Operations

@attr(optional=1) # Uses pandas
Expand Down
33 changes: 32 additions & 1 deletion tests/core/testutils.py
Expand Up @@ -17,7 +17,7 @@
from holoviews.core.util import (
sanitize_identifier_fn, find_range, max_range, wrap_tuple_streams,
deephash, merge_dimensions, get_path, make_path_unique, compute_density,
date_range, dt_to_int, compute_edges
date_range, dt_to_int, compute_edges, isfinite
)
from holoviews import Dimension, Element
from holoviews.streams import PointerXY
Expand Down Expand Up @@ -617,6 +617,37 @@ def test_date_range_1_sec(self):
self.assertEqual(drange[-1], end-np.timedelta64(50, 'ms'))


class TestNumericUtilities(ComparisonTestCase):

def test_isfinite_none(self):
self.assertFalse(isfinite(None))

def test_isfinite_nan(self):
self.assertFalse(isfinite(float('NaN')))

def test_isfinite_inf(self):
self.assertFalse(isfinite(float('inf')))

def test_isfinite_float(self):
self.assertTrue(isfinite(1.2))

def test_isfinite_float_array(self):
array = np.array([1.2, 3.0, np.NaN])
self.assertEqual(isfinite(array), np.array([True, True, False]))

def test_isfinite_datetime(self):
dt = datetime.datetime(2017, 1, 1)
self.assertTrue(isfinite(dt))

def test_isfinite_datetime64(self):
dt64 = np.datetime64(datetime.datetime(2017, 1, 1))
self.assertTrue(isfinite(dt64))

def test_isfinite_datetime64_array(self):
dt64 = np.array([np.datetime64(datetime.datetime(2017, 1, i)) for i in range(1, 4)])
self.assertEqual(isfinite(dt64), np.array([True, True, True]))


class TestComputeEdges(ComparisonTestCase):
"""
Tests for compute_edges function.
Expand Down

0 comments on commit 8711663

Please sign in to comment.