From 5b9718f0571ec4a9534bbbf0a241de94c9e1641c Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 09:32:21 -0400 Subject: [PATCH 01/49] enable vdim insertion in datasets --- holoviews/core/data/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 04db4206c7..9cdca0438d 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -446,13 +446,13 @@ def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs): Requires the dimension name or object, the desired position in the key dimensions and a key value scalar or array of values, - matching the length o shape of the Dataset. + matching the length or shape of the Dataset. Args: dimension: Dimension or dimension spec to add - dim_pos (int) Integer index to insert dimension at + dim_pos (int, None) Integer index to insert dimension at. Default: last dim_val (scalar or ndarray): Dimension value(s) to add - vdim: Disabled, this type does not have value dimensions + vdim: (bool) Whether to insert as vdim, otherwise as kdim **kwargs: Keyword arguments passed to the cloned element Returns: @@ -466,11 +466,14 @@ def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs): if vdim: dims = self.vdims[:] + if dim_pos is None: + dim_pos = len(self.kdims) dims.insert(dim_pos, dimension) dimensions = dict(vdims=dims) - dim_pos += self.ndims else: dims = self.kdims[:] + if dim_pos is None: + dim_pos = len(self.vdims) dims.insert(dim_pos, dimension) dimensions = dict(kdims=dims) From ec4f1862bb6c1bea85b50f833963b539a63dad98 Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 09:53:53 -0400 Subject: [PATCH 02/49] add transform to add new dimensions --- holoviews/core/data/__init__.py | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 9cdca0438d..0b5c795724 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -911,6 +911,47 @@ def load_subset(*args): return self.interface.groupby(self, dim_names, container_type, group_type, **kwargs) + def transform(self, output_signature=None, dim_transform=None, **kwargs): + """ + Transforms the Dataset according to a dimension transform. + + Args: + kwargs: Specify new dimensions in the form new_dim=dim_transform to assign the output directly + output_signature: Specify output arguments as a list of strings + dim_transform: a holoviews.util.transform.dim object + + Returns: + Transformed dataset with new dimensions + """ + if output_signature is None: + new_dimensions = OrderedDict([ + (dim_name, dim_transform.apply(self)) + for dim_name, dim_transform in kwargs.items() + ]) + elif dim_transform is not None: + # ineffectively repeating the same dim transform for now + if len(output_signature)==1: + new_dimensions = OrderedDict([ + (output_signature[0], dim_transform.apply(self)) + ]) + else: + new_dimensions = OrderedDict([ + (dim_name, dim_transform.apply(self)[k]) + for k, dim_name in enumerate(output_signature) + ]) + else: + raise ValueError('Need either kwargs or both output_signature and dim_transform') + + ds_new = self.clone() + for dim_name, dim_val in new_dimensions.items(): + ds_new = ds_new.add_dimension( + dimension=dim_name, + dim_pos=None, + dim_val=dim_val, + vdim=True, + ) + return ds_new + def __len__(self): "Number of values in the Dataset." return self.interface.length(self) From ad264aabddec6520c9a47362d7d1aa93b2cd144e Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 10:24:59 -0400 Subject: [PATCH 03/49] add option to traverse only first level of children --- holoviews/core/dimension.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index b19448e2e6..6733efa43e 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -630,7 +630,7 @@ def matches(self, spec): return identifier_match - def traverse(self, fn=None, specs=None, full_breadth=True): + def traverse(self, fn=None, specs=None, full_breadth=True, depth=None): """Traverses object returning matching items Traverses the set of children of the object, collecting the @@ -646,6 +646,8 @@ def traverse(self, fn=None, specs=None, full_breadth=True): full_breadth: Whether to traverse all objects Whether to traverse the full set of objects on each container or only the first. + depth (1 or None): Whether to traverse only the first level, + (excluding the parent level), or all Returns: list: List of objects that matched @@ -668,8 +670,13 @@ def traverse(self, fn=None, specs=None, full_breadth=True): for el in self: if el is None: continue - accumulator += el.traverse(fn, specs, full_breadth) + if depth is None: + accumulator += el.traverse(fn, specs, full_breadth) + elif depth == 1: + accumulator.append(el) if not full_breadth: break + if depth == 1: + accumulator = accumulator[1:] return accumulator From 555ef287208be275c8dd0f5abd2d15537d8b9e63 Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 10:25:26 -0400 Subject: [PATCH 04/49] add dim transform to dimensioned containers --- holoviews/core/dimension.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 6733efa43e..9395f05b6c 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -715,6 +715,26 @@ def map(self, map_fn, specs=None, clone=True): else: return map_fn(self) if applies else self + def transform(self, *args, **kwargs): + """ Transforms the contents according to a dimension transform. + + Applies a dimension transform to each child of this dimensioned container. + + Args: + output_signature (list of str): Specify output arguments + dim_transform (holoviews.util.transform.dim object) + kwargs: Specify new dimensions in the form new_dim=dim_transform + to assign the output directly + + Returns: + Container where each child has new dimensions + """ + new_self = self.clone() + for k, e in enumerate(self.traverse(depth=1)): + new_self[k] = e.transform(*args, **kwargs) + + return new_self + def __getstate__(self): "Ensures pickles save options applied to this objects." From 5cb209ce16abb124351944b082dc8e8724878fce Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 11:50:25 -0400 Subject: [PATCH 05/49] drop_dimensions method on pandas and xr interfaces --- holoviews/core/data/__init__.py | 16 ++++++++++++++++ holoviews/core/data/pandas.py | 6 ++++++ holoviews/core/data/xarray.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 0b5c795724..33e3a6cfb6 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -484,6 +484,22 @@ def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs): data = self.interface.add_dimension(self, dimension, dim_pos, dim_val, vdim) return self.clone(data, **dimensions) + def drop_dimensions(self, dimensions): + dimensions = [Dimension(drop_dim) for drop_dim in dimensions] + keep_kdims = [ + keep_dim + for keep_dim in self.kdims + if not keep_dim in dimensions + ] + keep_vdims = [ + keep_dim + for keep_dim in self.vdims + if not keep_dim in dimensions + ] + # if the backend requires handling of e.g. dependent variables, we can + # modify keep_*dims in each interface's implementation + data, keep_kdims, keep_vdims = self.interface.drop_dimensions(self, dimensions, keep_kdims, keep_vdims) + return self.clone(data=data, kdims=keep_kdims, vdims=keep_vdims) def select(self, selection_expr=None, selection_specs=None, **selection): """Applies selection by dimension name diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index b8934d7de2..65207577d0 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -345,6 +345,12 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): data.insert(dim_pos, dimension.name, values) return data + @classmethod + def drop_dimensions(cls, dataset, dimensions, keep_kdims=None, keep_vdims=None): + ds_new = dataset.data.drop(columns=[ + d.name for d in dimensions + ]) + return ds_new, keep_kdims, keep_vdims @classmethod def as_dframe(cls, dataset): diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index db2deb9603..a43d0d14a7 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -600,5 +600,37 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): dims=tuple(d.name for d in dataset.kdims[::-1])) return dataset.data.assign(**{dim: arr}) + @classmethod + def drop_dimensions(cls, dataset, dimensions, keep_kdims=None, keep_vdims=None): + data = dataset.data.copy() + dropped_dependent = False + dependent_dimensions = set() + for v in data: + dependent_dimensions = ( + dependent_dimensions + .union(set(data[v].dims)) + ) + + for d in dimensions: + if d in dataset.kdims: + if d in dependent_dimensions: + cls.param.warning( + 'Along with "%s", you are dropping dependent dimensions' + %d + ) + dropped_dependent = True + data = data.drop_dims(d) + elif d in dataset.vdims: + data = data.drop(d) + + if dropped_dependent: + keep_kdims = [ + d for d in dataset.kdims if d.name in data + ] + keep_vdims = [ + d for d in dataset.vdims if d.name in data + ] + return data, keep_kdims, keep_vdims + Interface.register(XArrayInterface) From c076a1b1c63262dc13cc04804b329d49b2c61633 Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 15:50:19 -0400 Subject: [PATCH 06/49] drop remaining dimensions after transform --- holoviews/core/data/__init__.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 33e3a6cfb6..2a73b9ce16 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -927,14 +927,21 @@ def load_subset(*args): return self.interface.groupby(self, dim_names, container_type, group_type, **kwargs) - def transform(self, output_signature=None, dim_transform=None, **kwargs): + def transform( + self, + output_signature=None, + dim_transform=None, + drop=False, + **kwargs + ): """ Transforms the Dataset according to a dimension transform. Args: - kwargs: Specify new dimensions in the form new_dim=dim_transform to assign the output directly output_signature: Specify output arguments as a list of strings dim_transform: a holoviews.util.transform.dim object + drop (bool): Whether to drop all variables not part of output + kwargs: Specify new dimensions in the form new_dim=dim_transform Returns: Transformed dataset with new dimensions @@ -966,6 +973,14 @@ def transform(self, output_signature=None, dim_transform=None, **kwargs): dim_val=dim_val, vdim=True, ) + + if drop: + new_dim_names = [Dimension(d).name for d in new_dimensions.keys()] + ds_new = ds_new.drop_dimensions( + [d for d in ds_new.dimensions() + if d.name not in new_dim_names] + ) + return ds_new def __len__(self): From bf0f4516b3ed10f72d9b1937a3286638039763ae Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 15:51:14 -0400 Subject: [PATCH 07/49] drop duplicate values after dropping dimensions --- holoviews/core/data/__init__.py | 19 +++++++-- holoviews/core/data/pandas.py | 8 ++-- holoviews/core/data/xarray.py | 72 +++++++++++++++++++++------------ 3 files changed, 67 insertions(+), 32 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 2a73b9ce16..74b7ff3d61 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -484,7 +484,15 @@ def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs): data = self.interface.add_dimension(self, dimension, dim_pos, dim_val, vdim) return self.clone(data, **dimensions) - def drop_dimensions(self, dimensions): + def drop_dimensions(self, dimensions, drop_duplicate_data=True): + """ + Drop dimensions from a Dataset. + + Args: + dimensions (list of str or Dimension): Dimensions to be dropped + drop_duplicate_data: Whether to remove duplicate data after dropping + dimensions + """ dimensions = [Dimension(drop_dim) for drop_dim in dimensions] keep_kdims = [ keep_dim @@ -498,8 +506,13 @@ def drop_dimensions(self, dimensions): ] # if the backend requires handling of e.g. dependent variables, we can # modify keep_*dims in each interface's implementation - data, keep_kdims, keep_vdims = self.interface.drop_dimensions(self, dimensions, keep_kdims, keep_vdims) - return self.clone(data=data, kdims=keep_kdims, vdims=keep_vdims) + data, keep_kdims, keep_vdims = self.interface.drop_dimensions( + self, dimensions, keep_kdims, keep_vdims, + drop_duplicate_data=drop_duplicate_data, + ) + return self.clone( + data=data, kdims=keep_kdims, vdims=keep_vdims, + ) def select(self, selection_expr=None, selection_specs=None, **selection): """Applies selection by dimension name diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index 65207577d0..1e2cd79ac4 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -346,11 +346,13 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): return data @classmethod - def drop_dimensions(cls, dataset, dimensions, keep_kdims=None, keep_vdims=None): - ds_new = dataset.data.drop(columns=[ + def drop_dimensions(cls, dataset, dimensions, keep_kdims=None, keep_vdims=None, drop_duplicate_data=True): + data = dataset.data.drop(columns=[ d.name for d in dimensions ]) - return ds_new, keep_kdims, keep_vdims + if drop_duplicate_data: + data = data.loc[~data.duplicated()] + return data, keep_kdims, keep_vdims @classmethod def as_dframe(cls, dataset): diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index a43d0d14a7..a3e87b5820 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -601,35 +601,55 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): return dataset.data.assign(**{dim: arr}) @classmethod - def drop_dimensions(cls, dataset, dimensions, keep_kdims=None, keep_vdims=None): + def drop_dimensions( + cls, dataset, dimensions, keep_kdims=None, keep_vdims=None, + drop_duplicate_data=True, + ): + """ + Drop dimensions from an xarray dataset. + + Note: + xarray automatically takes care of removing duplicate data that may + arise from removing key dimensions. + """ data = dataset.data.copy() - dropped_dependent = False - dependent_dimensions = set() - for v in data: - dependent_dimensions = ( - dependent_dimensions - .union(set(data[v].dims)) - ) - - for d in dimensions: - if d in dataset.kdims: - if d in dependent_dimensions: + dim_names = [d.name for d in dimensions] + + # replace constant values by non-dimensional array + import xarray as xr + for d in keep_vdims: + val = np.unique(data[d.name]) + if len(val) == 1: + data[d.name] = xr.DataArray(val[0], dims=[]) + + # utility to get the set of dimension names that are + # linked to variables in the data + def dependent_dimension_names(data): + dims = set() + for v in data: + dims = dims.union(set(data[v].dims)) + return dims + + # first drop vdims + for d in dataset.vdims: + if d in dim_names: + data = data.drop(d.name) + # now, some of the kdims may have become obsolete + for d in dataset.kdims: + if d in dim_names: + if d.name in dependent_dimension_names(data): cls.param.warning( - 'Along with "%s", you are dropping dependent dimensions' - %d + 'Not dropping "%s" as it has dependent dimensions' %d ) - dropped_dependent = True - data = data.drop_dims(d) - elif d in dataset.vdims: - data = data.drop(d) - - if dropped_dependent: - keep_kdims = [ - d for d in dataset.kdims if d.name in data - ] - keep_vdims = [ - d for d in dataset.vdims if d.name in data - ] + else: + data = data.drop_dims(d.name) + + keep_kdims = [ + d for d in dataset.kdims if d.name in data + ] + keep_vdims = [ + d for d in dataset.vdims if d.name in data + ] return data, keep_kdims, keep_vdims From cc2347843e266aa0bf592d2861fcce692bfdf52e Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 15:51:39 -0400 Subject: [PATCH 08/49] move computation of multi-output transform out of loop --- holoviews/core/data/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 74b7ff3d61..da9d0e386a 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -965,14 +965,14 @@ def transform( for dim_name, dim_transform in kwargs.items() ]) elif dim_transform is not None: - # ineffectively repeating the same dim transform for now if len(output_signature)==1: new_dimensions = OrderedDict([ (output_signature[0], dim_transform.apply(self)) ]) else: + dim_transform_output = dim_transform.apply(self) new_dimensions = OrderedDict([ - (dim_name, dim_transform.apply(self)[k]) + (dim_name, dim_transform_output[k]) for k, dim_name in enumerate(output_signature) ]) else: From db7e8beaa3bcf0ba9b0f317f43028752f717c86d Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 15:52:19 -0400 Subject: [PATCH 09/49] annotation --- holoviews/core/data/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index da9d0e386a..90b497cf26 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -987,6 +987,8 @@ def transform( vdim=True, ) + # dropping dimensions will also take care of potentially arising + # duplicate data points if drop: new_dim_names = [Dimension(d).name for d in new_dimensions.keys()] ds_new = ds_new.drop_dimensions( From c9314f29535f8d46e8de45b65e3cede84c032a5f Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 15:52:42 -0400 Subject: [PATCH 10/49] fix dimension insertion index --- holoviews/core/data/pandas.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index 1e2cd79ac4..0dc447c28f 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -341,6 +341,8 @@ def sample(cls, dataset, samples=[]): @classmethod def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): data = dataset.data.copy() + if vdim: + dim_pos += len(dataset.kdims) if dimension.name not in data: data.insert(dim_pos, dimension.name, values) return data From 89f26eb1a57fbe226fe690bb6bb49c0c0c4e856f Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 16:39:50 -0400 Subject: [PATCH 11/49] enable dropping of dims and duplicates in ndmapping transforms --- holoviews/core/data/__init__.py | 5 +++-- holoviews/core/dimension.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 90b497cf26..a5e4312440 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -988,12 +988,13 @@ def transform( ) # dropping dimensions will also take care of potentially arising - # duplicate data points + # duplicate data points if drop_duplicate_data is True if drop: new_dim_names = [Dimension(d).name for d in new_dimensions.keys()] ds_new = ds_new.drop_dimensions( [d for d in ds_new.dimensions() - if d.name not in new_dim_names] + if d.name not in new_dim_names], + drop_duplicate_data=drop_duplicate_data, ) return ds_new diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 9395f05b6c..456617e9cf 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -715,14 +715,17 @@ def map(self, map_fn, specs=None, clone=True): else: return map_fn(self) if applies else self - def transform(self, *args, **kwargs): - """ Transforms the contents according to a dimension transform. + def transform(self, *args, drop=False, drop_duplicate_data=True, **kwargs): + """Transforms the contents according to a dimension transform. Applies a dimension transform to each child of this dimensioned container. Args: output_signature (list of str): Specify output arguments dim_transform (holoviews.util.transform.dim object) + drop (bool): Whether to drop all variables not part of output + drop_duplicate_data (bool): Whether to drop duplicate data (if + non-output variables are dropped, see argument `bool`) kwargs: Specify new dimensions in the form new_dim=dim_transform to assign the output directly @@ -730,8 +733,13 @@ def transform(self, *args, **kwargs): Container where each child has new dimensions """ new_self = self.clone() - for k, e in enumerate(self.traverse(depth=1)): - new_self[k] = e.transform(*args, **kwargs) + for key, el in self.items(): + new_self[key] = el.transform( + *args, + drop=drop, + drop_duplicate_data=drop_duplicate_data, + **kwargs, + ) return new_self From 31cca921cace4be8dbf358c5a05957a66fe3d04a Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 16:58:43 -0400 Subject: [PATCH 12/49] Enable Dataset aggregation using dim transforms --- holoviews/core/data/__init__.py | 133 ++++++++++++++++++++------------ 1 file changed, 82 insertions(+), 51 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index a5e4312440..e565118936 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -826,11 +826,17 @@ def reduce(self, dimensions=[], function=None, spreadfn=None, **reductions): return self.aggregate(dims, function, spreadfn) - def aggregate(self, dimensions=None, function=None, spreadfn=None, **kwargs): + def aggregate( + self, dimensions=None, + function=None, spreadfn=None, + dim_transform=None, dim_transform_signature=None, + **kwargs, + ): """Aggregates data on the supplied dimensions. Aggregates over the supplied key dimensions with the defined - function. + function or dim_transform. If neither is given explicitly, we can assign + aggregated variables in the form var_name=dim_transform. Args: dimensions: Dimension(s) to aggregate on @@ -839,56 +845,78 @@ def aggregate(self, dimensions=None, function=None, spreadfn=None, **kwargs): spreadfn: Secondary reduction to compute value spread Useful for computing a confidence interval, spread, or standard deviation. - **kwargs: Keyword arguments passed to the aggregation function + dim_transform: holoviews.utils.transform.dim object + dim_transform_signature: list of str specifying output arguments + **kwargs: Keyword arguments either passed to the aggregation function + or to create new names for the transformed variables Returns: Returns the aggregated Dataset """ + if function is not None and dim_transform is not None: + raise ValueError('Can only specify at most one of function or dim_transform') if function is None: - raise ValueError("The aggregate method requires a function to be specified") - if dimensions is None: dimensions = self.kdims - elif not isinstance(dimensions, list): dimensions = [dimensions] - kdims = [self.get_dimension(d, strict=True) for d in dimensions] - if not len(self): + # Handle dim transforms + faceted = self.clone().groupby(dimensions) + drop_args = dict(drop=True, drop_duplicate_data=True) + if dim_transform is None: + # If both function and dim_transform are None, + # we assume that kwargs must hold dim_transform assignments + return faceted.transform(**drop_args, **kwargs).collapse() + else: + return ( + faceted + .transform( + dim_transform=dim_transform, signature=dim_transform_signature, + **drop_args, + ) + .collapse() + ) + else: + # Handle functions + if dimensions is None: dimensions = self.kdims + elif not isinstance(dimensions, list): dimensions = [dimensions] + kdims = [self.get_dimension(d, strict=True) for d in dimensions] + if not len(self): + if spreadfn: + spread_name = spreadfn.__name__ + vdims = [d for vd in self.vdims for d in [vd, vd.clone('_'.join([vd.name, spread_name]))]] + else: + vdims = self.vdims + return self.clone([], kdims=kdims, vdims=vdims) + + vdims = self.vdims + aggregated, dropped = self.interface.aggregate(self, kdims, function, **kwargs) + aggregated = self.interface.unpack_scalar(self, aggregated) + vdims = [vd for vd in vdims if vd not in dropped] + + ndims = len(dimensions) + min_d, max_d = self.param.objects('existing')['kdims'].bounds + generic_type = (min_d is not None and ndims < min_d) or (max_d is not None and ndims > max_d) + if spreadfn: + error, _ = self.interface.aggregate(self, dimensions, spreadfn) spread_name = spreadfn.__name__ - vdims = [d for vd in self.vdims for d in [vd, vd.clone('_'.join([vd.name, spread_name]))]] + ndims = len(vdims) + error = self.clone(error, kdims=kdims, new_type=Dataset) + combined = self.clone(aggregated, kdims=kdims, new_type=Dataset) + for i, d in enumerate(vdims): + dim = d.clone('_'.join([d.name, spread_name])) + dvals = error.dimension_values(d, flat=False) + combined = combined.add_dimension(dim, ndims+i, dvals, True) + return combined.clone(new_type=Dataset if generic_type else type(self)) + + if np.isscalar(aggregated): + return aggregated else: - vdims = self.vdims - return self.clone([], kdims=kdims, vdims=vdims) - - vdims = self.vdims - aggregated, dropped = self.interface.aggregate(self, kdims, function, **kwargs) - aggregated = self.interface.unpack_scalar(self, aggregated) - vdims = [vd for vd in vdims if vd not in dropped] - - ndims = len(dimensions) - min_d, max_d = self.param.objects('existing')['kdims'].bounds - generic_type = (min_d is not None and ndims < min_d) or (max_d is not None and ndims > max_d) - - if spreadfn: - error, _ = self.interface.aggregate(self, dimensions, spreadfn) - spread_name = spreadfn.__name__ - ndims = len(vdims) - error = self.clone(error, kdims=kdims, new_type=Dataset) - combined = self.clone(aggregated, kdims=kdims, new_type=Dataset) - for i, d in enumerate(vdims): - dim = d.clone('_'.join([d.name, spread_name])) - dvals = error.dimension_values(d, flat=False) - combined = combined.add_dimension(dim, ndims+i, dvals, True) - return combined.clone(new_type=Dataset if generic_type else type(self)) - - if np.isscalar(aggregated): - return aggregated - else: - try: - # Should be checking the dimensions declared on the element are compatible - return self.clone(aggregated, kdims=kdims, vdims=vdims) - except: - datatype = self.param.objects('existing')['datatype'].default - return self.clone(aggregated, kdims=kdims, vdims=vdims, - new_type=Dataset if generic_type else None, - datatype=datatype) + try: + # Should be checking the dimensions declared on the element are compatible + return self.clone(aggregated, kdims=kdims, vdims=vdims) + except: + datatype = self.param.objects('existing')['datatype'].default + return self.clone(aggregated, kdims=kdims, vdims=vdims, + new_type=Dataset if generic_type else None, + datatype=datatype) def groupby(self, dimensions=[], container_type=HoloMap, group_type=None, @@ -942,41 +970,44 @@ def load_subset(*args): def transform( self, - output_signature=None, + signature=None, dim_transform=None, drop=False, + drop_duplicate_data=True, **kwargs ): """ Transforms the Dataset according to a dimension transform. Args: - output_signature: Specify output arguments as a list of strings + signature: Specify output arguments as a list of strings dim_transform: a holoviews.util.transform.dim object drop (bool): Whether to drop all variables not part of output + drop_duplicate_data (bool): Whether to drop duplicate data (if + non-output variables are dropped, see argument `bool`) kwargs: Specify new dimensions in the form new_dim=dim_transform Returns: Transformed dataset with new dimensions """ - if output_signature is None: + if signature is None: new_dimensions = OrderedDict([ (dim_name, dim_transform.apply(self)) for dim_name, dim_transform in kwargs.items() ]) elif dim_transform is not None: - if len(output_signature)==1: + if len(signature)==1: new_dimensions = OrderedDict([ - (output_signature[0], dim_transform.apply(self)) + (signature[0], dim_transform.apply(self)) ]) else: dim_transform_output = dim_transform.apply(self) new_dimensions = OrderedDict([ (dim_name, dim_transform_output[k]) - for k, dim_name in enumerate(output_signature) + for k, dim_name in enumerate(signature) ]) else: - raise ValueError('Need either kwargs or both output_signature and dim_transform') + raise ValueError('Need either kwargs or both signature and dim_transform') ds_new = self.clone() for dim_name, dim_val in new_dimensions.items(): From 71eac0d94b51d07e17f6dda0fb51f4043ec48fd1 Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 19:17:16 -0400 Subject: [PATCH 13/49] aggregation defaults to all kdims --- holoviews/core/data/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index e565118936..7c18514d56 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -855,6 +855,9 @@ def aggregate( """ if function is not None and dim_transform is not None: raise ValueError('Can only specify at most one of function or dim_transform') + + if dimensions is None: dimensions = self.kdims + elif not isinstance(dimensions, list): dimensions = [dimensions] if function is None: # Handle dim transforms faceted = self.clone().groupby(dimensions) @@ -874,8 +877,6 @@ def aggregate( ) else: # Handle functions - if dimensions is None: dimensions = self.kdims - elif not isinstance(dimensions, list): dimensions = [dimensions] kdims = [self.get_dimension(d, strict=True) for d in dimensions] if not len(self): if spreadfn: From eb579038213099ea6074dd84e64e81401512f05b Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 19:18:19 -0400 Subject: [PATCH 14/49] handle dataset without dim restrictions during aggregation --- holoviews/core/data/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 7c18514d56..c963701eeb 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -860,7 +860,7 @@ def aggregate( elif not isinstance(dimensions, list): dimensions = [dimensions] if function is None: # Handle dim transforms - faceted = self.clone().groupby(dimensions) + faceted = self.clone(new_type=Dataset).groupby(dimensions) drop_args = dict(drop=True, drop_duplicate_data=True) if dim_transform is None: # If both function and dim_transform are None, @@ -873,7 +873,7 @@ def aggregate( dim_transform=dim_transform, signature=dim_transform_signature, **drop_args, ) - .collapse() + .collapse().clone(new_type=type(self)) ) else: # Handle functions From eb78673d89fb6db0e69fe13f10b1245c7fda5536 Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 19:18:41 -0400 Subject: [PATCH 15/49] allow single string for transform output signature --- holoviews/core/data/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index c963701eeb..80bf5f0d3a 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -997,6 +997,8 @@ def transform( for dim_name, dim_transform in kwargs.items() ]) elif dim_transform is not None: + if isinstance(signature, (str, Dimension)): + signature = [signature] if len(signature)==1: new_dimensions = OrderedDict([ (signature[0], dim_transform.apply(self)) From 029fb3f4ae3cccead42e8d640071330aa467e1e3 Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 19:22:37 -0400 Subject: [PATCH 16/49] enable bokeh hextiles aggregation with dim transforms --- holoviews/plotting/bokeh/hex_tiles.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/bokeh/hex_tiles.py b/holoviews/plotting/bokeh/hex_tiles.py index 091dc89ee8..b056b732c0 100644 --- a/holoviews/plotting/bokeh/hex_tiles.py +++ b/holoviews/plotting/bokeh/hex_tiles.py @@ -1,4 +1,5 @@ from __future__ import absolute_import, division, unicode_literals +import types import param import numpy as np @@ -8,6 +9,7 @@ except: cartesian_to_axial = None +from ...util.transform import dim as dim_transform from ...core import Dimension, Operation from ...core.options import Compositor from ...core.util import basestring, isfinite @@ -26,7 +28,10 @@ class hex_binning(Operation): useable. """ - aggregator = param.Callable(default=np.size) + aggregator = param.ClassSelector(default=np.size, class_=(dim, types.FunctionType), + doc=""" + Aggregation function or dimension transform used to compute bin values. + Defaults to np.size to count the number of values in each bin.""") gridsize = param.ClassSelector(default=50, class_=(int, tuple)) @@ -80,7 +85,17 @@ def _process(self, element, key=None): xd, yd = (element.get_dimension(i) for i in indexes) xd, yd = xd.clone(range=(x0, x1)), yd.clone(range=(y0, y1)) kdims = [yd, xd] if self.p.invert_axes else [xd, yd] - agg = element.clone(data, kdims=kdims, vdims=vdims).aggregate(function=aggregator) + if isinstance(aggregator, dim_transform): + agg_args = dict( + dim_transform=aggregator, + dim_transform_signature=['_Transformed'], + ) + else: + agg_args = dict(function=aggregator) + agg = ( + element.clone(data, kdims=kdims, vdims=vdims) + .aggregate(**agg_args) + ) if self.p.min_count is not None and self.p.min_count > 1: agg = agg[:, :, self.p.min_count:] return agg @@ -95,10 +110,10 @@ def _process(self, element, key=None): class HexTilesPlot(ColorbarPlot): - aggregator = param.Callable(default=np.size, doc=""" - Aggregation function used to compute bin values. Any NumPy - reduction is allowed, defaulting to np.size to count the number - of values in each bin.""") + aggregator = param.ClassSelector(default=np.size, class_=(dim, types.FunctionType), + doc=""" + Aggregation function or dimension transform used to compute bin values. + Defaults to np.size to count the number of values in each bin.""") gridsize = param.ClassSelector(default=50, class_=(int, tuple), doc=""" Number of hexagonal bins along x- and y-axes. Defaults to uniform From 347f59248626e5c2f32e37d3c9e710472645e8f2 Mon Sep 17 00:00:00 2001 From: poplarShift Date: Wed, 30 Oct 2019 21:55:54 -0400 Subject: [PATCH 17/49] enable overwriting of dataset dimensions --- holoviews/core/data/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 80bf5f0d3a..2619fb3726 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -461,9 +461,6 @@ def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs): if isinstance(dimension, (util.basestring, tuple)): dimension = Dimension(dimension) - if dimension.name in self.kdims: - raise Exception('{dim} dimension already defined'.format(dim=dimension.name)) - if vdim: dims = self.vdims[:] if dim_pos is None: @@ -479,9 +476,13 @@ def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs): if issubclass(self.interface, ArrayInterface) and np.asarray(dim_val).dtype != self.data.dtype: element = self.clone(datatype=[default_datatype]) - data = element.interface.add_dimension(element, dimension, dim_pos, dim_val, vdim) else: - data = self.interface.add_dimension(self, dimension, dim_pos, dim_val, vdim) + element = self.clone() + + if dimension.name in element.dimensions(): + # self.param.warning('Overwriting existing "{dim}" dimension'.format(dim=dimension.name)) + element = element.drop_dimensions([dimension.name]) + data = element.interface.add_dimension(element, dimension, dim_pos, dim_val, vdim) return self.clone(data, **dimensions) def drop_dimensions(self, dimensions, drop_duplicate_data=True): From 805907b90c73576c11eca8751cbfe6d3049f9544 Mon Sep 17 00:00:00 2001 From: poplarShift Date: Thu, 31 Oct 2019 08:31:11 -0400 Subject: [PATCH 18/49] enable passing dim transforms to hex tiles --- holoviews/plotting/bokeh/hex_tiles.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/hex_tiles.py b/holoviews/plotting/bokeh/hex_tiles.py index b056b732c0..2f6f97ddd5 100644 --- a/holoviews/plotting/bokeh/hex_tiles.py +++ b/holoviews/plotting/bokeh/hex_tiles.py @@ -33,6 +33,11 @@ class hex_binning(Operation): Aggregation function or dimension transform used to compute bin values. Defaults to np.size to count the number of values in each bin.""") + aggregator_signature = param.List(default=None, class_=str, doc=""" + Names for output variables of aggregator. Can be referenced in dim + transforms on the element. Only respected when aggregator is itself a dim + transform.""") + gridsize = param.ClassSelector(default=50, class_=(int, tuple)) invert_axes = param.Boolean(default=False) @@ -86,9 +91,12 @@ def _process(self, element, key=None): xd, yd = xd.clone(range=(x0, x1)), yd.clone(range=(y0, y1)) kdims = [yd, xd] if self.p.invert_axes else [xd, yd] if isinstance(aggregator, dim_transform): + signature = self.p.aggregator_signature + if signature is None: + signature = vdims[:1] agg_args = dict( dim_transform=aggregator, - dim_transform_signature=['_Transformed'], + dim_transform_signature=signature, ) else: agg_args = dict(function=aggregator) @@ -115,6 +123,11 @@ class HexTilesPlot(ColorbarPlot): Aggregation function or dimension transform used to compute bin values. Defaults to np.size to count the number of values in each bin.""") + aggregator_signature = param.List(default=None, class_=str, doc=""" + Names for output variables of aggregator. Can be referenced in dim + transforms on the element. Only respected when aggregator is itself a dim + transform.""") + gridsize = param.ClassSelector(default=50, class_=(int, tuple), doc=""" Number of hexagonal bins along x- and y-axes. Defaults to uniform sampling along both axes when setting and integer but independent From ea00cb5faf1567ed761d1732944b62b7cca13a16 Mon Sep 17 00:00:00 2001 From: poplarShift Date: Thu, 31 Oct 2019 08:50:46 -0400 Subject: [PATCH 19/49] fix dim insertion position --- holoviews/core/data/__init__.py | 4 ++-- holoviews/core/data/pandas.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 2619fb3726..f167efaa52 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -464,13 +464,13 @@ def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs): if vdim: dims = self.vdims[:] if dim_pos is None: - dim_pos = len(self.kdims) + dim_pos = len(self.vdims) + len(self.kdims) dims.insert(dim_pos, dimension) dimensions = dict(vdims=dims) else: dims = self.kdims[:] if dim_pos is None: - dim_pos = len(self.vdims) + dim_pos = len(self.kdims) dims.insert(dim_pos, dimension) dimensions = dict(kdims=dims) diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index 0dc447c28f..1e2cd79ac4 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -341,8 +341,6 @@ def sample(cls, dataset, samples=[]): @classmethod def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): data = dataset.data.copy() - if vdim: - dim_pos += len(dataset.kdims) if dimension.name not in data: data.insert(dim_pos, dimension.name, values) return data From 3a8d4d282e2b34ed7260f024be0bf3e700c16fa4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 16 Jan 2020 01:29:07 +0100 Subject: [PATCH 20/49] Transforms improvements --- holoviews/core/accessors.py | 13 ++++++++-- holoviews/core/data/__init__.py | 45 +++++++++++---------------------- holoviews/core/dimension.py | 28 -------------------- 3 files changed, 26 insertions(+), 60 deletions(-) diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py index 2fc9720044..316aed5131 100644 --- a/holoviews/core/accessors.py +++ b/holoviews/core/accessors.py @@ -205,17 +205,26 @@ def apply_function(object, **kwargs): mapped.append((k, new_val)) return self._obj.clone(mapped, link=link_inputs) - def aggregate(self, dimensions=None, function=None, spreadfn=None, **kwargs): """Applies a aggregate function to all ViewableElements. - See :py:meth:`Dimensioned.opts` and :py:meth:`Apply.__call__` + See :py:meth:`Dimensioned.aggregate` and :py:meth:`Apply.__call__` for more information. """ kwargs['_method_args'] = (dimensions, function, spreadfn) kwargs['per_element'] = True return self.__call__('aggregate', **kwargs) + def transform(self, *args, drop=False, drop_duplicate_data=True, **kwargs): + """Applies transforms to all Datasets. + + See :py:meth:`Dataset.transform` and :py:meth:`Apply.__call__` + for more information. + """ + kwargs['_method_args'] = args + return self._call__('transform', drop=drop, drop_duplicate_data=drop_duplicate_data, + spec=Dataset, **kwargs) + def opts(self, *args, **kwargs): """Applies options to all ViewableElement objects. diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index f167efaa52..b0a5cbd614 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -970,48 +970,33 @@ def load_subset(*args): return self.interface.groupby(self, dim_names, container_type, group_type, **kwargs) - def transform( - self, - signature=None, - dim_transform=None, - drop=False, - drop_duplicate_data=True, - **kwargs - ): + def transform(self, *args, drop=False, drop_duplicate_data=True, **kwargs): """ Transforms the Dataset according to a dimension transform. Args: - signature: Specify output arguments as a list of strings - dim_transform: a holoviews.util.transform.dim object + args: Specify the output arguments and transforms as a + tuple of dimension specs and dim transforms drop (bool): Whether to drop all variables not part of output drop_duplicate_data (bool): Whether to drop duplicate data (if - non-output variables are dropped, see argument `bool`) + non-output variables are dropped, see argument `drop`) kwargs: Specify new dimensions in the form new_dim=dim_transform Returns: Transformed dataset with new dimensions """ - if signature is None: - new_dimensions = OrderedDict([ - (dim_name, dim_transform.apply(self)) - for dim_name, dim_transform in kwargs.items() - ]) - elif dim_transform is not None: - if isinstance(signature, (str, Dimension)): - signature = [signature] - if len(signature)==1: - new_dimensions = OrderedDict([ - (signature[0], dim_transform.apply(self)) - ]) + transforms = OrderedDict() + for s, transform in list(args)+list(kwargs.items()): + transforms[wrap_tuple(s)] = transform + + new_dimensions = OrderedDict() + for signature, transform in transforms.items(): + applied = dim_transform.apply(self) + if len(s) == 1: + new_dimensions[s[0]] = applied else: - dim_transform_output = dim_transform.apply(self) - new_dimensions = OrderedDict([ - (dim_name, dim_transform_output[k]) - for k, dim_name in enumerate(signature) - ]) - else: - raise ValueError('Need either kwargs or both signature and dim_transform') + for s, vals in zip(signature, applied): + new_dimensions[s] = vals ds_new = self.clone() for dim_name, dim_val in new_dimensions.items(): diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 456617e9cf..6733efa43e 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -715,34 +715,6 @@ def map(self, map_fn, specs=None, clone=True): else: return map_fn(self) if applies else self - def transform(self, *args, drop=False, drop_duplicate_data=True, **kwargs): - """Transforms the contents according to a dimension transform. - - Applies a dimension transform to each child of this dimensioned container. - - Args: - output_signature (list of str): Specify output arguments - dim_transform (holoviews.util.transform.dim object) - drop (bool): Whether to drop all variables not part of output - drop_duplicate_data (bool): Whether to drop duplicate data (if - non-output variables are dropped, see argument `bool`) - kwargs: Specify new dimensions in the form new_dim=dim_transform - to assign the output directly - - Returns: - Container where each child has new dimensions - """ - new_self = self.clone() - for key, el in self.items(): - new_self[key] = el.transform( - *args, - drop=drop, - drop_duplicate_data=drop_duplicate_data, - **kwargs, - ) - - return new_self - def __getstate__(self): "Ensures pickles save options applied to this objects." From 0e191a6967422433d544cabb87f8a8daa3bfa956 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 13:31:03 +0100 Subject: [PATCH 21/49] Implement assign based transform method --- holoviews/core/accessors.py | 19 ++- holoviews/core/data/__init__.py | 200 ++++++++++---------------- holoviews/core/data/array.py | 12 ++ holoviews/core/data/dictionary.py | 9 +- holoviews/core/data/pandas.py | 11 +- holoviews/core/data/xarray.py | 58 ++------ holoviews/core/dimension.py | 15 +- holoviews/plotting/bokeh/hex_tiles.py | 33 +---- 8 files changed, 121 insertions(+), 236 deletions(-) diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py index 316aed5131..1c50ab1fab 100644 --- a/holoviews/core/accessors.py +++ b/holoviews/core/accessors.py @@ -215,16 +215,6 @@ def aggregate(self, dimensions=None, function=None, spreadfn=None, **kwargs): kwargs['per_element'] = True return self.__call__('aggregate', **kwargs) - def transform(self, *args, drop=False, drop_duplicate_data=True, **kwargs): - """Applies transforms to all Datasets. - - See :py:meth:`Dataset.transform` and :py:meth:`Apply.__call__` - for more information. - """ - kwargs['_method_args'] = args - return self._call__('transform', drop=drop, drop_duplicate_data=drop_duplicate_data, - spec=Dataset, **kwargs) - def opts(self, *args, **kwargs): """Applies options to all ViewableElement objects. @@ -262,6 +252,15 @@ def select(self, **kwargs): """ return self.__call__('select', **kwargs) + def transform(self, *args, drop=False, **kwargs): + """Applies transforms to all Datasets. + + See :py:meth:`Dataset.transform` and :py:meth:`Apply.__call__` + for more information. + """ + kwargs['_method_args'] = args + kwargs['per_element'] = True + return self.__call__('transform', drop=drop, **kwargs) @add_metaclass(AccessorPipelineMeta) class Redim(object): diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index b0a5cbd614..1c1b9c30b5 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -485,36 +485,6 @@ def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs): data = element.interface.add_dimension(element, dimension, dim_pos, dim_val, vdim) return self.clone(data, **dimensions) - def drop_dimensions(self, dimensions, drop_duplicate_data=True): - """ - Drop dimensions from a Dataset. - - Args: - dimensions (list of str or Dimension): Dimensions to be dropped - drop_duplicate_data: Whether to remove duplicate data after dropping - dimensions - """ - dimensions = [Dimension(drop_dim) for drop_dim in dimensions] - keep_kdims = [ - keep_dim - for keep_dim in self.kdims - if not keep_dim in dimensions - ] - keep_vdims = [ - keep_dim - for keep_dim in self.vdims - if not keep_dim in dimensions - ] - # if the backend requires handling of e.g. dependent variables, we can - # modify keep_*dims in each interface's implementation - data, keep_kdims, keep_vdims = self.interface.drop_dimensions( - self, dimensions, keep_kdims, keep_vdims, - drop_duplicate_data=drop_duplicate_data, - ) - return self.clone( - data=data, kdims=keep_kdims, vdims=keep_vdims, - ) - def select(self, selection_expr=None, selection_specs=None, **selection): """Applies selection by dimension name @@ -827,12 +797,7 @@ def reduce(self, dimensions=[], function=None, spreadfn=None, **reductions): return self.aggregate(dims, function, spreadfn) - def aggregate( - self, dimensions=None, - function=None, spreadfn=None, - dim_transform=None, dim_transform_signature=None, - **kwargs, - ): + def aggregate(self, dimensions=None, function=None, spreadfn=None, **kwargs): """Aggregates data on the supplied dimensions. Aggregates over the supplied key dimensions with the defined @@ -842,83 +807,72 @@ def aggregate( Args: dimensions: Dimension(s) to aggregate on Default to all key dimensions - function: Aggregation function to apply, e.g. numpy.mean + function: Aggregation function or transform to apply + Supports both simple functions and dimension transforms spreadfn: Secondary reduction to compute value spread Useful for computing a confidence interval, spread, or standard deviation. - dim_transform: holoviews.utils.transform.dim object - dim_transform_signature: list of str specifying output arguments **kwargs: Keyword arguments either passed to the aggregation function or to create new names for the transformed variables Returns: Returns the aggregated Dataset """ - if function is not None and dim_transform is not None: - raise ValueError('Can only specify at most one of function or dim_transform') - + from ...util.transform import dim if dimensions is None: dimensions = self.kdims elif not isinstance(dimensions, list): dimensions = [dimensions] - if function is None: - # Handle dim transforms - faceted = self.clone(new_type=Dataset).groupby(dimensions) - drop_args = dict(drop=True, drop_duplicate_data=True) - if dim_transform is None: - # If both function and dim_transform are None, - # we assume that kwargs must hold dim_transform assignments - return faceted.transform(**drop_args, **kwargs).collapse() - else: - return ( - faceted - .transform( - dim_transform=dim_transform, signature=dim_transform_signature, - **drop_args, - ) - .collapse().clone(new_type=type(self)) - ) - else: - # Handle functions - kdims = [self.get_dimension(d, strict=True) for d in dimensions] - if not len(self): - if spreadfn: - spread_name = spreadfn.__name__ - vdims = [d for vd in self.vdims for d in [vd, vd.clone('_'.join([vd.name, spread_name]))]] - else: - vdims = self.vdims - return self.clone([], kdims=kdims, vdims=vdims) - - vdims = self.vdims - aggregated, dropped = self.interface.aggregate(self, kdims, function, **kwargs) - aggregated = self.interface.unpack_scalar(self, aggregated) - vdims = [vd for vd in vdims if vd not in dropped] - - ndims = len(dimensions) - min_d, max_d = self.param.objects('existing')['kdims'].bounds - generic_type = (min_d is not None and ndims < min_d) or (max_d is not None and ndims > max_d) - + if isinstance(function, tuple) or any(isinstance(v, dim) for v in kwargs.values()): + dataset = self.clone(new_type=Dataset) + if dimensions: + dataset = dataset.groupby(dimensions) + args = () if function is None else (function,) + transformed = dataset.apply.transform(*args, drop=True, **kwargs) + if not isinstance(transformed, Dataset): + transformed = transformed.collapse() + return transformed.clone(new_type=type(self)) + + # Handle functions + kdims = [self.get_dimension(d, strict=True) for d in dimensions] + if not len(self): if spreadfn: - error, _ = self.interface.aggregate(self, dimensions, spreadfn) spread_name = spreadfn.__name__ - ndims = len(vdims) - error = self.clone(error, kdims=kdims, new_type=Dataset) - combined = self.clone(aggregated, kdims=kdims, new_type=Dataset) - for i, d in enumerate(vdims): - dim = d.clone('_'.join([d.name, spread_name])) - dvals = error.dimension_values(d, flat=False) - combined = combined.add_dimension(dim, ndims+i, dvals, True) - return combined.clone(new_type=Dataset if generic_type else type(self)) - - if np.isscalar(aggregated): - return aggregated + vdims = [d for vd in self.vdims for d in [vd, vd.clone('_'.join([vd.name, spread_name]))]] else: - try: - # Should be checking the dimensions declared on the element are compatible - return self.clone(aggregated, kdims=kdims, vdims=vdims) - except: - datatype = self.param.objects('existing')['datatype'].default - return self.clone(aggregated, kdims=kdims, vdims=vdims, - new_type=Dataset if generic_type else None, - datatype=datatype) + vdims = self.vdims + return self.clone([], kdims=kdims, vdims=vdims) + + vdims = self.vdims + aggregated, dropped = self.interface.aggregate(self, kdims, function, **kwargs) + aggregated = self.interface.unpack_scalar(self, aggregated) + vdims = [vd for vd in vdims if vd not in dropped] + + ndims = len(dimensions) + min_d, max_d = self.param.objects('existing')['kdims'].bounds + generic_type = (min_d is not None and ndims < min_d) or (max_d is not None and ndims > max_d) + + if spreadfn: + error, _ = self.interface.aggregate(self, dimensions, spreadfn) + spread_name = spreadfn.__name__ + ndims = len(vdims) + error = self.clone(error, kdims=kdims, new_type=Dataset) + combined = self.clone(aggregated, kdims=kdims, new_type=Dataset) + for i, d in enumerate(vdims): + dim = d.clone('_'.join([d.name, spread_name])) + dvals = error.dimension_values(d, flat=False) + combined = combined.add_dimension(dim, ndims+i, dvals, True) + return combined.clone(new_type=Dataset if generic_type else type(self)) + + if np.isscalar(aggregated): + return aggregated + else: + try: + # Should be checking the dimensions declared on the element are compatible + return self.clone(aggregated, kdims=kdims, vdims=vdims) + except: + datatype = self.param.objects('existing')['datatype'].default + return self.clone(aggregated, kdims=kdims, vdims=vdims, + new_type=Dataset if generic_type else None, + datatype=datatype) def groupby(self, dimensions=[], container_type=HoloMap, group_type=None, @@ -970,7 +924,7 @@ def load_subset(*args): return self.interface.groupby(self, dim_names, container_type, group_type, **kwargs) - def transform(self, *args, drop=False, drop_duplicate_data=True, **kwargs): + def transform(self, *args, drop=False, **kwargs): """ Transforms the Dataset according to a dimension transform. @@ -978,8 +932,6 @@ def transform(self, *args, drop=False, drop_duplicate_data=True, **kwargs): args: Specify the output arguments and transforms as a tuple of dimension specs and dim transforms drop (bool): Whether to drop all variables not part of output - drop_duplicate_data (bool): Whether to drop duplicate data (if - non-output variables are dropped, see argument `drop`) kwargs: Specify new dimensions in the form new_dim=dim_transform Returns: @@ -987,37 +939,29 @@ def transform(self, *args, drop=False, drop_duplicate_data=True, **kwargs): """ transforms = OrderedDict() for s, transform in list(args)+list(kwargs.items()): - transforms[wrap_tuple(s)] = transform + transforms[util.wrap_tuple(s)] = transform - new_dimensions = OrderedDict() + new_data = OrderedDict() for signature, transform in transforms.items(): - applied = dim_transform.apply(self) - if len(s) == 1: - new_dimensions[s[0]] = applied + applied = transform.apply(self, compute=False, keep_index=True) + if len(signature) == 1: + new_data[s[0]] = applied else: for s, vals in zip(signature, applied): - new_dimensions[s] = vals - - ds_new = self.clone() - for dim_name, dim_val in new_dimensions.items(): - ds_new = ds_new.add_dimension( - dimension=dim_name, - dim_pos=None, - dim_val=dim_val, - vdim=True, - ) - - # dropping dimensions will also take care of potentially arising - # duplicate data points if drop_duplicate_data is True + new_data[s] = vals + + new_dims = [] + for d in new_data: + if self.get_dimension(s) is None: + new_dims.append(d) + if drop: - new_dim_names = [Dimension(d).name for d in new_dimensions.keys()] - ds_new = ds_new.drop_dimensions( - [d for d in ds_new.dimensions() - if d.name not in new_dim_names], - drop_duplicate_data=drop_duplicate_data, - ) - - return ds_new + kdims = [self.get_dimension(d) for d in new_data if d in self.kdims] + vdims = [self.get_dimension(d) or d for d in new_data if d not in self.kdims] + return self.clone(new_data, kdims=kdims, vdims=vdims) + else: + data = self.interface.assign(self, new_data) + return self.clone(data, vdims=self.vdims+new_dims) def __len__(self): "Number of values in the Dataset." diff --git a/holoviews/core/data/array.py b/holoviews/core/data/array.py index da03c3e932..75c9d53420 100644 --- a/holoviews/core/data/array.py +++ b/holoviews/core/data/array.py @@ -239,6 +239,18 @@ def unpack_scalar(cls, dataset, data): return data + @classmethod + def assign(cls, dataset, new_data): + data = dataset.data.copy() + for d, arr in new_data.items(): + if dataset.get_dimension(d) is None: + continue + idx = dataset.get_dimension_index(d) + data[:, idx] = arr + new_cols = [arr for d, arr in new_data.items() if dataset.get_dimension(d) is None] + return np.column_stack([data]+new_cols) + + @classmethod def aggregate(cls, dataset, dimensions, function, **kwargs): reindexed = dataset.reindex(dimensions) diff --git a/holoviews/core/data/dictionary.py b/holoviews/core/data/dictionary.py index 6a4d38501f..8cd14cefcf 100644 --- a/holoviews/core/data/dictionary.py +++ b/holoviews/core/data/dictionary.py @@ -215,7 +215,7 @@ def concat(cls, datasets, dimensions, vdims): columns = defaultdict(list) for key, ds in datasets: for k, vals in ds.data.items(): - columns[k].append(vals) + columns[k].append(np.atleast_1d(vals)) for d, k in zip(dimensions, key): columns[d.name].append(np.full(len(ds), k)) @@ -270,6 +270,13 @@ def values(cls, dataset, dim, expanded=True, flat=True, compute=True, keep_index return values + @classmethod + def assign(cls, dataset, new_data): + data = OrderedDict(dataset.data) + data.update(new_data) + return data + + @classmethod def reindex(cls, dataset, kdims, vdims): dimensions = [dataset.get_dimension(d).name for d in kdims+vdims] diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index 1e2cd79ac4..ad1c318ea7 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -179,7 +179,7 @@ def concat_fn(cls, dataframes, **kwargs): kwargs['sort'] = False return pd.concat(dataframes, **kwargs) - + @classmethod def concat(cls, datasets, dimensions, vdims): dataframes = [] @@ -346,13 +346,8 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): return data @classmethod - def drop_dimensions(cls, dataset, dimensions, keep_kdims=None, keep_vdims=None, drop_duplicate_data=True): - data = dataset.data.drop(columns=[ - d.name for d in dimensions - ]) - if drop_duplicate_data: - data = data.loc[~data.duplicated()] - return data, keep_kdims, keep_vdims + def assign(cls, dataset, new_data): + return dataset.data.assign(**new_data) @classmethod def as_dframe(cls, dataset): diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index a3e87b5820..4b4ce20408 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -601,56 +601,14 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): return dataset.data.assign(**{dim: arr}) @classmethod - def drop_dimensions( - cls, dataset, dimensions, keep_kdims=None, keep_vdims=None, - drop_duplicate_data=True, - ): - """ - Drop dimensions from an xarray dataset. - - Note: - xarray automatically takes care of removing duplicate data that may - arise from removing key dimensions. - """ - data = dataset.data.copy() - dim_names = [d.name for d in dimensions] - - # replace constant values by non-dimensional array - import xarray as xr - for d in keep_vdims: - val = np.unique(data[d.name]) - if len(val) == 1: - data[d.name] = xr.DataArray(val[0], dims=[]) - - # utility to get the set of dimension names that are - # linked to variables in the data - def dependent_dimension_names(data): - dims = set() - for v in data: - dims = dims.union(set(data[v].dims)) - return dims - - # first drop vdims - for d in dataset.vdims: - if d in dim_names: - data = data.drop(d.name) - # now, some of the kdims may have become obsolete - for d in dataset.kdims: - if d in dim_names: - if d.name in dependent_dimension_names(data): - cls.param.warning( - 'Not dropping "%s" as it has dependent dimensions' %d - ) - else: - data = data.drop_dims(d.name) - - keep_kdims = [ - d for d in dataset.kdims if d.name in data - ] - keep_vdims = [ - d for d in dataset.vdims if d.name in data - ] - return data, keep_kdims, keep_vdims + def assign(cls, dataset, new_data): + data = dataset.data + coords = {k: v for k, v in new_data.items() if k in dataset.kdims} + if coords: + data = data.assign_coords(coords) + vars = {k: v for k, v in new_data.items() if k not in dataset.kdims} + data = data.assign(vars) + return data Interface.register(XArrayInterface) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 6733efa43e..b88558bd64 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -629,14 +629,11 @@ def matches(self, spec): identifier_match = match_fn(identifier_specification[:len(split_spec)]) == self_spec return identifier_match - - def traverse(self, fn=None, specs=None, full_breadth=True, depth=None): + def traverse(self, fn=None, specs=None, full_breadth=True): """Traverses object returning matching items - Traverses the set of children of the object, collecting the all objects matching the defined specs. Each object can be processed with the supplied function. - Args: fn (function, optional): Function applied to matched objects specs: List of specs to match @@ -646,9 +643,6 @@ def traverse(self, fn=None, specs=None, full_breadth=True, depth=None): full_breadth: Whether to traverse all objects Whether to traverse the full set of objects on each container or only the first. - depth (1 or None): Whether to traverse only the first level, - (excluding the parent level), or all - Returns: list: List of objects that matched """ @@ -670,13 +664,8 @@ def traverse(self, fn=None, specs=None, full_breadth=True, depth=None): for el in self: if el is None: continue - if depth is None: - accumulator += el.traverse(fn, specs, full_breadth) - elif depth == 1: - accumulator.append(el) + accumulator += el.traverse(fn, specs, full_breadth) if not full_breadth: break - if depth == 1: - accumulator = accumulator[1:] return accumulator diff --git a/holoviews/plotting/bokeh/hex_tiles.py b/holoviews/plotting/bokeh/hex_tiles.py index 2f6f97ddd5..6988be0f5e 100644 --- a/holoviews/plotting/bokeh/hex_tiles.py +++ b/holoviews/plotting/bokeh/hex_tiles.py @@ -28,15 +28,11 @@ class hex_binning(Operation): useable. """ - aggregator = param.ClassSelector(default=np.size, class_=(dim, types.FunctionType), - doc=""" - Aggregation function or dimension transform used to compute bin values. - Defaults to np.size to count the number of values in each bin.""") - - aggregator_signature = param.List(default=None, class_=str, doc=""" - Names for output variables of aggregator. Can be referenced in dim - transforms on the element. Only respected when aggregator is itself a dim - transform.""") + aggregator = param.ClassSelector( + default=np.size, class_=(dim, types.FunctionType, tuple), doc=""" + Aggregation function or dimension transform used to compute + bin values. Defaults to np.size to count the number of values + in each bin.""") gridsize = param.ClassSelector(default=50, class_=(int, tuple)) @@ -90,19 +86,9 @@ def _process(self, element, key=None): xd, yd = (element.get_dimension(i) for i in indexes) xd, yd = xd.clone(range=(x0, x1)), yd.clone(range=(y0, y1)) kdims = [yd, xd] if self.p.invert_axes else [xd, yd] - if isinstance(aggregator, dim_transform): - signature = self.p.aggregator_signature - if signature is None: - signature = vdims[:1] - agg_args = dict( - dim_transform=aggregator, - dim_transform_signature=signature, - ) - else: - agg_args = dict(function=aggregator) agg = ( element.clone(data, kdims=kdims, vdims=vdims) - .aggregate(**agg_args) + .aggregate(function=aggregator) ) if self.p.min_count is not None and self.p.min_count > 1: agg = agg[:, :, self.p.min_count:] @@ -118,16 +104,11 @@ def _process(self, element, key=None): class HexTilesPlot(ColorbarPlot): - aggregator = param.ClassSelector(default=np.size, class_=(dim, types.FunctionType), + aggregator = param.ClassSelector(default=np.size, class_=(dim, types.FunctionType, tuple), doc=""" Aggregation function or dimension transform used to compute bin values. Defaults to np.size to count the number of values in each bin.""") - aggregator_signature = param.List(default=None, class_=str, doc=""" - Names for output variables of aggregator. Can be referenced in dim - transforms on the element. Only respected when aggregator is itself a dim - transform.""") - gridsize = param.ClassSelector(default=50, class_=(int, tuple), doc=""" Number of hexagonal bins along x- and y-axes. Defaults to uniform sampling along both axes when setting and integer but independent From 7bf8fcd4880c7c71082a23e23b85f9f0dafdbea1 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 13:56:38 +0100 Subject: [PATCH 22/49] Further cleanup --- holoviews/core/data/__init__.py | 47 +++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 1c1b9c30b5..aef38d3a34 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -14,7 +14,7 @@ from .. import util from ..accessors import Redim from ..dimension import ( - Dimension, process_dimensions, Dimensioned, LabelledData + Dimension, Dimensioned, LabelledData, dimension_name, process_dimensions ) from ..element import Element from ..ndmapping import OrderedDict, MultiDimensionalMapping @@ -450,41 +450,37 @@ def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs): Args: dimension: Dimension or dimension spec to add - dim_pos (int, None) Integer index to insert dimension at. Default: last + dim_pos (int) Integer index to insert dimension at dim_val (scalar or ndarray): Dimension value(s) to add - vdim: (bool) Whether to insert as vdim, otherwise as kdim + vdim: Disabled, this type does not have value dimensions **kwargs: Keyword arguments passed to the cloned element - Returns: Cloned object containing the new dimension """ if isinstance(dimension, (util.basestring, tuple)): dimension = Dimension(dimension) + if dimension.name in self.kdims: + raise Exception('{dim} dimension already defined'.format(dim=dimension.name)) + if vdim: dims = self.vdims[:] - if dim_pos is None: - dim_pos = len(self.vdims) + len(self.kdims) dims.insert(dim_pos, dimension) dimensions = dict(vdims=dims) + dim_pos += self.ndims else: dims = self.kdims[:] - if dim_pos is None: - dim_pos = len(self.kdims) dims.insert(dim_pos, dimension) dimensions = dict(kdims=dims) if issubclass(self.interface, ArrayInterface) and np.asarray(dim_val).dtype != self.data.dtype: element = self.clone(datatype=[default_datatype]) + data = element.interface.add_dimension(element, dimension, dim_pos, dim_val, vdim) else: - element = self.clone() - - if dimension.name in element.dimensions(): - # self.param.warning('Overwriting existing "{dim}" dimension'.format(dim=dimension.name)) - element = element.drop_dimensions([dimension.name]) - data = element.interface.add_dimension(element, dimension, dim_pos, dim_val, vdim) + data = self.interface.add_dimension(self, dimension, dim_pos, dim_val, vdim) return self.clone(data, **dimensions) + def select(self, selection_expr=None, selection_specs=None, **selection): """Applies selection by dimension name @@ -801,8 +797,8 @@ def aggregate(self, dimensions=None, function=None, spreadfn=None, **kwargs): """Aggregates data on the supplied dimensions. Aggregates over the supplied key dimensions with the defined - function or dim_transform. If neither is given explicitly, we can assign - aggregated variables in the form var_name=dim_transform. + function or dim_transform specified as tuple of the transformed + dimension name and dim transform. Args: dimensions: Dimension(s) to aggregate on @@ -925,13 +921,22 @@ def load_subset(*args): group_type, **kwargs) def transform(self, *args, drop=False, **kwargs): - """ - Transforms the Dataset according to a dimension transform. + """Transforms the Dataset according to a dimension transform. + + Transforms may be supplied as tuples consisting of the + dimension(s) and the dim transform to apply or keyword + arguments mapping from dimension(s) to dim transforms. If the + arg or kwarg declares multiple dimensions the dim transform + should return a tuple of values for each. + + A transform may override an existing dimension or add a new + one in which case it will be added as an additional value + dimension. Args: args: Specify the output arguments and transforms as a tuple of dimension specs and dim transforms - drop (bool): Whether to drop all variables not part of output + drop (bool): Whether to drop all variables not part of the transform kwargs: Specify new dimensions in the form new_dim=dim_transform Returns: @@ -958,8 +963,10 @@ def transform(self, *args, drop=False, **kwargs): if drop: kdims = [self.get_dimension(d) for d in new_data if d in self.kdims] vdims = [self.get_dimension(d) or d for d in new_data if d not in self.kdims] - return self.clone(new_data, kdims=kdims, vdims=vdims) + data = OrderedDict([(dimension_name(d), values) for d, values in new_data.items()]) + return self.clone(data, kdims=kdims, vdims=vdims) else: + new_data = OrderedDict([(dimension_name(d), values) for d, values in new_data.items()]) data = self.interface.assign(self, new_data) return self.clone(data, vdims=self.vdims+new_dims) From 0109fd29c2792a6ecf79392ed5540a77ee988289 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 13:59:54 +0100 Subject: [PATCH 23/49] Fixed flake --- holoviews/plotting/bokeh/hex_tiles.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/holoviews/plotting/bokeh/hex_tiles.py b/holoviews/plotting/bokeh/hex_tiles.py index 6988be0f5e..50a0e60cdd 100644 --- a/holoviews/plotting/bokeh/hex_tiles.py +++ b/holoviews/plotting/bokeh/hex_tiles.py @@ -9,12 +9,11 @@ except: cartesian_to_axial = None -from ...util.transform import dim as dim_transform from ...core import Dimension, Operation from ...core.options import Compositor from ...core.util import basestring, isfinite from ...element import HexTiles -from ...util.transform import dim +from ...util.transform import dim as dim_transform from .element import ColorbarPlot from .selection import BokehOverlaySelectionDisplay from .styles import line_properties, fill_properties @@ -29,10 +28,10 @@ class hex_binning(Operation): """ aggregator = param.ClassSelector( - default=np.size, class_=(dim, types.FunctionType, tuple), doc=""" - Aggregation function or dimension transform used to compute - bin values. Defaults to np.size to count the number of values - in each bin.""") + default=np.size, class_=(types.FunctionType, tuple), doc=""" + Aggregation function or dimension transform used to compute bin + values. Defaults to np.size to count the number of values + in each bin.""") gridsize = param.ClassSelector(default=50, class_=(int, tuple)) @@ -104,10 +103,11 @@ def _process(self, element, key=None): class HexTilesPlot(ColorbarPlot): - aggregator = param.ClassSelector(default=np.size, class_=(dim, types.FunctionType, tuple), - doc=""" - Aggregation function or dimension transform used to compute bin values. - Defaults to np.size to count the number of values in each bin.""") + aggregator = param.ClassSelector( + default=np.size, class_=(types.FunctionType, tuple), doc=""" + Aggregation function or dimension transform used to compute + bin values. Defaults to np.size to count the number of values + in each bin.""") gridsize = param.ClassSelector(default=50, class_=(int, tuple), doc=""" Number of hexagonal bins along x- and y-axes. Defaults to uniform From 6a97267e15e3c88f329c0e0056b469d335c9d81a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 14:22:00 +0100 Subject: [PATCH 24/49] Added tests --- holoviews/core/data/__init__.py | 4 ++-- holoviews/core/data/xarray.py | 3 ++- holoviews/tests/core/data/base.py | 31 ++++++++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index aef38d3a34..e097e92749 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -950,14 +950,14 @@ def transform(self, *args, drop=False, **kwargs): for signature, transform in transforms.items(): applied = transform.apply(self, compute=False, keep_index=True) if len(signature) == 1: - new_data[s[0]] = applied + new_data[signature[0]] = applied else: for s, vals in zip(signature, applied): new_data[s] = vals new_dims = [] for d in new_data: - if self.get_dimension(s) is None: + if self.get_dimension(d) is None: new_dims.append(d) if drop: diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 4b4ce20408..14a9c49509 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -606,7 +606,8 @@ def assign(cls, dataset, new_data): coords = {k: v for k, v in new_data.items() if k in dataset.kdims} if coords: data = data.assign_coords(coords) - vars = {k: v for k, v in new_data.items() if k not in dataset.kdims} + vars = {k: (tuple(kd.name for kd in dataset.kdims[::-1]), v) + for k, v in new_data.items() if k not in dataset.kdims} data = data.assign(vars) return data diff --git a/holoviews/tests/core/data/base.py b/holoviews/tests/core/data/base.py index 0a68e72e72..8cdc6f526d 100644 --- a/holoviews/tests/core/data/base.py +++ b/holoviews/tests/core/data/base.py @@ -395,7 +395,17 @@ def test_dataset_get_dframe_by_dimension(self): df = self.dataset_hm.dframe(['x']) self.assertEqual(df, pd.DataFrame({'x': self.xs}, dtype=df.dtypes[0])) + def test_dataset_transform_replace_hm(self): + transformed = self.dataset_hm.transform(y=dim('y')*2) + expected = Dataset((self.xs, self.y_ints*2), 'x', 'y') + self.assertEqual(transformed, expected) + def test_dataset_transform_add_hm(self): + transformed = self.dataset_hm.transform(y2=dim('y')*2) + expected = Dataset((self.xs, self.y_ints, self.y_ints*2), 'x', ['y', 'y2']) + self.assertEqual(transformed, expected) + + class HeterogeneousColumnTests(HomogeneousColumnTests): """ @@ -613,7 +623,6 @@ def test_dataset_groupby(self): grouped = HoloMap([('M', Dataset(group1, kdims=['Age'], vdims=self.vdims)), ('F', Dataset(group2, kdims=['Age'], vdims=self.vdims))], kdims=['Gender'], sort=False) - print(grouped.keys()) self.assertEqual(self.table.groupby(['Gender']), grouped) def test_dataset_groupby_alias(self): @@ -840,6 +849,26 @@ def test_dataset_array_ht(self): self.assertEqual(self.dataset_ht.array(), np.column_stack([self.xs, self.ys])) + # Transforms + + def test_dataset_transform_replace_ht(self): + transformed = self.table.transform( + Age=dim('Age')**2, Weight=dim('Weight')*2, Height=dim('Height')/2. + ) + expected = Dataset({'Gender':self.gender, 'Age':self.age**2, + 'Weight':self.weight*2, 'Height':self.height/2.}, + kdims=self.kdims, vdims=self.vdims) + self.assertEqual(transformed, expected) + + def test_dataset_transform_add_ht(self): + transformed = self.table.transform(combined=dim('Age')*dim('Weight')) + expected = Dataset({'Gender':self.gender, 'Age':self.age, + 'Weight':self.weight, 'Height':self.height, + 'combined': self.age*self.weight}, + kdims=self.kdims, vdims=self.vdims+['combined']) + self.assertEqual(transformed, expected) + + class ScalarColumnTests(object): """ From 2a4cbef1fa2edf62beebbcfc368a1660733fecb7 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 14:35:01 +0100 Subject: [PATCH 25/49] Fixed flakes --- holoviews/plotting/bokeh/hex_tiles.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/hex_tiles.py b/holoviews/plotting/bokeh/hex_tiles.py index 50a0e60cdd..2df7ddd24b 100644 --- a/holoviews/plotting/bokeh/hex_tiles.py +++ b/holoviews/plotting/bokeh/hex_tiles.py @@ -203,7 +203,8 @@ def get_data(self, element, ranges, style): style['aspect_scale'] = scale scale_dim = element.get_dimension(self.size_index) scale = style.get('scale') - if scale_dim and ((isinstance(scale, basestring) and scale in element) or isinstance(scale, dim)): + if (scale_dim and ((isinstance(scale, basestring) and scale in element) or + isinstance(scale, dim_transform))): self.param.warning("Cannot declare style mapping for 'scale' option " "and declare a size_index; ignoring the size_index.") scale_dim = None From d5924f29bf500e23d9ad1242c32affd80c502193 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 15:33:28 +0100 Subject: [PATCH 26/49] Python2 fix --- holoviews/core/data/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index e097e92749..3c5fcf5281 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -920,7 +920,7 @@ def load_subset(*args): return self.interface.groupby(self, dim_names, container_type, group_type, **kwargs) - def transform(self, *args, drop=False, **kwargs): + def transform(self, *args, **kwargs): """Transforms the Dataset according to a dimension transform. Transforms may be supplied as tuples consisting of the @@ -942,6 +942,7 @@ def transform(self, *args, drop=False, **kwargs): Returns: Transformed dataset with new dimensions """ + drop = kwargs.pop('drop') transforms = OrderedDict() for s, transform in list(args)+list(kwargs.items()): transforms[util.wrap_tuple(s)] = transform From b6bd400981c542abe55f347a0c887630ffeb2a39 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 15:35:23 +0100 Subject: [PATCH 27/49] Fix handling of grid transforms --- holoviews/core/data/grid.py | 24 ++++++++++-- holoviews/core/data/xarray.py | 21 ++++++++-- .../tests/core/data/testgridinterface.py | 39 ++++++++++++++++++- 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/holoviews/core/data/grid.py b/holoviews/core/data/grid.py index 48a544d38a..12ac6447d3 100644 --- a/holoviews/core/data/grid.py +++ b/holoviews/core/data/grid.py @@ -411,7 +411,8 @@ def ndloc(cls, dataset, indices): @classmethod - def values(cls, dataset, dim, expanded=True, flat=True, compute=True, keep_index=False): + def values(cls, dataset, dim, expanded=True, flat=True, compute=True, + keep_index=False, canonicalize=True): dim = dataset.get_dimension(dim, strict=True) if dim in dataset.vdims or dataset.data[dim.name].ndim > 1: vdim_tuple = cls.packed(dataset) @@ -419,16 +420,17 @@ def values(cls, dataset, dim, expanded=True, flat=True, compute=True, keep_index data = dataset.data[vdim_tuple][..., dataset.vdims.index(dim)] else: data = dataset.data[dim.name] - data = cls.canonicalize(dataset, data) + if canonicalize: + data = cls.canonicalize(dataset, data) da = dask_array_module() if compute and da and isinstance(data, da.Array): data = data.compute() return data.T.flatten() if flat else data elif expanded: - data = cls.coords(dataset, dim.name, expanded=True) + data = cls.coords(dataset, dim.name, expanded=True, ordered=canonicalize) return data.T.flatten() if flat else data else: - return cls.coords(dataset, dim.name, ordered=True) + return cls.coords(dataset, dim.name, ordered=canonicalize) @classmethod @@ -798,5 +800,19 @@ def range(cls, dataset, dimension): column.sort() return column[0], column[-1] + @classmethod + def assign(cls, dataset, new_data): + data = OrderedDict(dataset.data) + for k, v in new_data.items(): + if k in dataset.kdims: + coords = cls.coords(dataset, k) + if not coords.ndim > 1 and np.all(coords[1:] < coords[:-1]): + v = v[::-1] + data[k] = v + else: + data[k] = cls.canonicalize(dataset, v) + return data + + Interface.register(GridInterface) diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 14a9c49509..71c373489e 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -603,12 +603,25 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): @classmethod def assign(cls, dataset, new_data): data = dataset.data - coords = {k: v for k, v in new_data.items() if k in dataset.kdims} + coords = OrderedDict() + for k, v in new_data.items(): + if k not in dataset.kdims: + continue + coord_vals = cls.coords(dataset, k) + if not coord_vals.ndim > 1 and np.all(coord_vals[1:] < coord_vals[:-1]): + v = v[::-1] + coords[k] = (k, v) if coords: data = data.assign_coords(coords) - vars = {k: (tuple(kd.name for kd in dataset.kdims[::-1]), v) - for k, v in new_data.items() if k not in dataset.kdims} - data = data.assign(vars) + packed = cls.packed(dataset) + dims = tuple(kd.name for kd in dataset.kdims[::-1]) + vars = OrderedDict() + for k, v in new_data.items(): + if k in dataset.kdims: + continue + vars[k] = (dims, cls.canonicalize(dataset, v, data_coords=dims)) + if vars: + data = data.assign(vars) return data diff --git a/holoviews/tests/core/data/testgridinterface.py b/holoviews/tests/core/data/testgridinterface.py index 6cab4a593d..bc8aa2f49b 100644 --- a/holoviews/tests/core/data/testgridinterface.py +++ b/holoviews/tests/core/data/testgridinterface.py @@ -8,6 +8,7 @@ from holoviews.core.data import Dataset from holoviews.core.util import pd, date_range from holoviews.element import Image, Curve, RGB, HSV +from holoviews.util.transform import dim try: import dask.array as da @@ -314,9 +315,44 @@ def test_mask_2d_array_xy_reversed(self): expected[mask] = np.nan self.assertEqual(masked_array, expected) + def test_dataset_transform_replace_kdim_on_grid(self): + transformed = self.dataset_grid.transform(x=dim('x')*2) + expected = self.element( + ([0, 2], self.grid_ys, self.grid_zs), ['x', 'y'], ['z'] + ) + self.assertEqual(transformed, expected) + + def test_dataset_transform_replace_vdim_on_grid(self): + transformed = self.dataset_grid.transform(z=dim('z')*2) + expected = self.element( + (self.grid_xs, self.grid_ys, self.grid_zs*2), ['x', 'y'], ['z'] + ) + self.assertEqual(transformed, expected) + + def test_dataset_transform_replace_vdim_on_grid(self): + transformed = self.dataset_grid.transform(z=dim('z')*2) + expected = self.element( + (self.grid_xs, self.grid_ys, self.grid_zs*2), ['x', 'y'], ['z'] + ) + self.assertEqual(transformed, expected) + + def test_dataset_transform_replace_kdim_on_inverted_grid(self): + transformed = self.dataset_grid_inv.transform(x=dim('x')*2) + expected = self.element( + ([2, 0], self.grid_ys[::-1], self.grid_zs), ['x', 'y'], ['z'] + ) + self.assertEqual(transformed, expected) + + def test_dataset_transform_replace_vdim_on_inverted_grid(self): + transformed = self.dataset_grid_inv.transform(z=dim('z')*2) + expected = self.element( + (self.grid_xs[::-1], self.grid_ys[::-1], self.grid_zs*2), ['x', 'y'], ['z'] + ) + self.assertEqual(transformed, expected) -class GridInterfaceTests(BaseGridInterfaceTests): + +class GridInterfaceTests(BaseGridInterfaceTests): datatype = 'grid' data_type = (OrderedDict, dict) element = Dataset @@ -498,6 +534,7 @@ def test_dataset_get_dframe(self): + class ImageElement_GridInterfaceTests(BaseImageElementInterfaceTests): datatype = 'grid' From 47ae6babfd62962e99f25c3b43ce839969bbbda6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 15:43:04 +0100 Subject: [PATCH 28/49] Fix flakes --- holoviews/core/data/xarray.py | 1 - holoviews/tests/core/data/testgridinterface.py | 7 ------- 2 files changed, 8 deletions(-) diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 71c373489e..264af0c257 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -613,7 +613,6 @@ def assign(cls, dataset, new_data): coords[k] = (k, v) if coords: data = data.assign_coords(coords) - packed = cls.packed(dataset) dims = tuple(kd.name for kd in dataset.kdims[::-1]) vars = OrderedDict() for k, v in new_data.items(): diff --git a/holoviews/tests/core/data/testgridinterface.py b/holoviews/tests/core/data/testgridinterface.py index bc8aa2f49b..428a5a4f97 100644 --- a/holoviews/tests/core/data/testgridinterface.py +++ b/holoviews/tests/core/data/testgridinterface.py @@ -329,13 +329,6 @@ def test_dataset_transform_replace_vdim_on_grid(self): ) self.assertEqual(transformed, expected) - def test_dataset_transform_replace_vdim_on_grid(self): - transformed = self.dataset_grid.transform(z=dim('z')*2) - expected = self.element( - (self.grid_xs, self.grid_ys, self.grid_zs*2), ['x', 'y'], ['z'] - ) - self.assertEqual(transformed, expected) - def test_dataset_transform_replace_kdim_on_inverted_grid(self): transformed = self.dataset_grid_inv.transform(x=dim('x')*2) expected = self.element( From 96f15590091f97407a408d170c13c7d3706b23c3 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 15:51:25 +0100 Subject: [PATCH 29/49] Small fix --- holoviews/core/data/__init__.py | 15 +++++++++------ holoviews/tests/core/data/testgridinterface.py | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 3c5fcf5281..7ee63068bd 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -942,7 +942,7 @@ def transform(self, *args, **kwargs): Returns: Transformed dataset with new dimensions """ - drop = kwargs.pop('drop') + drop = kwargs.pop('drop', False) transforms = OrderedDict() for s, transform in list(args)+list(kwargs.items()): transforms[util.wrap_tuple(s)] = transform @@ -961,15 +961,18 @@ def transform(self, *args, **kwargs): if self.get_dimension(d) is None: new_dims.append(d) + if self.interface.datatype in ('image', 'array'): + ds = self.clone(datatype=[dt for dt in self.datatype if dt != self.interface.datatype]) + if drop: - kdims = [self.get_dimension(d) for d in new_data if d in self.kdims] - vdims = [self.get_dimension(d) or d for d in new_data if d not in self.kdims] + kdims = [ds.get_dimension(d) for d in new_data if d in ds.kdims] + vdims = [ds.get_dimension(d) or d for d in new_data if d not in ds.kdims] data = OrderedDict([(dimension_name(d), values) for d, values in new_data.items()]) - return self.clone(data, kdims=kdims, vdims=vdims) + return ds.clone(data, kdims=kdims, vdims=vdims) else: new_data = OrderedDict([(dimension_name(d), values) for d, values in new_data.items()]) - data = self.interface.assign(self, new_data) - return self.clone(data, vdims=self.vdims+new_dims) + data = ds.interface.assign(ds, new_data) + return ds.clone(data, vdims=ds.vdims+new_dims) def __len__(self): "Number of values in the Dataset." diff --git a/holoviews/tests/core/data/testgridinterface.py b/holoviews/tests/core/data/testgridinterface.py index 428a5a4f97..9321a01fa2 100644 --- a/holoviews/tests/core/data/testgridinterface.py +++ b/holoviews/tests/core/data/testgridinterface.py @@ -335,7 +335,7 @@ def test_dataset_transform_replace_kdim_on_inverted_grid(self): ([2, 0], self.grid_ys[::-1], self.grid_zs), ['x', 'y'], ['z'] ) self.assertEqual(transformed, expected) - + def test_dataset_transform_replace_vdim_on_inverted_grid(self): transformed = self.dataset_grid_inv.transform(z=dim('z')*2) expected = self.element( From e35af036815108ac2960177b04d720e2aee83607 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 16:09:29 +0100 Subject: [PATCH 30/49] Add more tests --- holoviews/core/data/__init__.py | 5 +++-- .../tests/core/data/testbinneddatasets.py | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 7ee63068bd..8458212c79 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -961,8 +961,9 @@ def transform(self, *args, **kwargs): if self.get_dimension(d) is None: new_dims.append(d) - if self.interface.datatype in ('image', 'array'): - ds = self.clone(datatype=[dt for dt in self.datatype if dt != self.interface.datatype]) + ds = self + if ds.interface.datatype in ('image', 'array'): + ds = ds.clone(datatype=[dt for dt in ds.datatype if dt != ds.interface.datatype]) if drop: kdims = [ds.get_dimension(d) for d in new_data if d in ds.kdims] diff --git a/holoviews/tests/core/data/testbinneddatasets.py b/holoviews/tests/core/data/testbinneddatasets.py index fc593f6fea..da20b08d61 100644 --- a/holoviews/tests/core/data/testbinneddatasets.py +++ b/holoviews/tests/core/data/testbinneddatasets.py @@ -13,6 +13,7 @@ from holoviews.core.util import OrderedDict from holoviews.element import Histogram, QuadMesh from holoviews.element.comparison import ComparisonTestCase +from holoviews.util.transform import dim class Binned1DTest(ComparisonTestCase): @@ -156,7 +157,17 @@ def test_groupby_ydim(self): for i in range(3)}, kdims=['y']) self.assertEqual(grouped, holomap) + def test_qmesh_transform_replace_kdim(self): + transformed = self.dataset2d.transform(x=dim('x')*2) + expected = QuadMesh((self.xs*2, self.ys, self.zs)) + self.assertEqual(expected, transformed) + def test_qmesh_transform_replace_vdim(self): + transformed = self.dataset2d.transform(z=dim('z')*2) + expected = QuadMesh((self.xs, self.ys, self.zs*2)) + self.assertEqual(expected, transformed) + + class Irregular2DBinsTest(ComparisonTestCase): @@ -253,3 +264,13 @@ def test_groupby_3d_from_xarray(self): hmap = HoloMap({0: Dataset((self.xs, self.ys, zs[0]), ['lon', 'lat'], 'A'), 1: Dataset((self.xs, self.ys, zs[1]), ['lon', 'lat'], 'A')}, kdims='z') self.assertEqual(grouped, hmap) + + def test_irregular_transform_replace_kdim(self): + transformed = Dataset((self.xs, self.ys, self.zs), ['x', 'y'], 'z').transform(x=dim('x')*2) + expected = Dataset((self.xs*2, self.ys, self.zs), ['x', 'y'], 'z') + self.assertEqual(expected, transformed) + + def test_irregular_transform_replace_vdim(self): + transformed = Dataset((self.xs, self.ys, self.zs), ['x', 'y'], 'z').transform(z=dim('z')*2) + expected = Dataset((self.xs, self.ys, self.zs*2), ['x', 'y'], 'z') + self.assertEqual(expected, transformed) From d867e640f5ca0701ebc0a01742948771b2d86f49 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 17:41:23 +0100 Subject: [PATCH 31/49] Allow arbitrary dim expressions --- holoviews/util/transform.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index 8c38eb7ec5..264a4d2374 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -2,6 +2,7 @@ import operator +from functools import partial from types import BuiltinFunctionType, BuiltinMethodType, FunctionType, MethodType import numpy as np @@ -222,7 +223,7 @@ def __init__(self, obj, *args, **kwargs): else: fn = None if fn is not None: - if not (isinstance(fn, function_types) or + if not (isinstance(fn, function_types+(basestring,)) or any(fn in funcs for funcs in self._all_funcs)): raise ValueError('Second argument must be a function, ' 'found %s type' % type(fn)) @@ -268,6 +269,14 @@ def pipe(cls, func, *args, **kwargs): def __hash__(self): return hash(repr(self)) + def __getattr__(self, attr): + if attr in self.__dict__: + return self.__dict__[attr] + return partial(self.method, attr) + + def method(self, method, *args, **kwargs): + return dim(self, method, *args, **kwargs) + # Builtin functions def __abs__(self): return dim(self, abs) def __round__(self, ndigits=None): @@ -491,11 +500,15 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, eldim = dataset.get_dimension(dimension) drange = ranges.get(eldim.name, {}) drange = drange.get('combined', drange) + fn = o['fn'] kwargs = o['kwargs'] - if ((o['fn'] is norm) or (o['fn'] is lognorm)) and drange != {} and not ('min' in kwargs and 'max' in kwargs): - data = o['fn'](data, *drange) + if (((fn is norm) or (o['fn'] is lognorm)) and drange != {} and + not ('min' in kwargs and 'max' in kwargs)): + data = fn(data, *drange) + elif isinstance(fn, basestring): + data = getattr(data, fn)(*args[1:], **kwargs) else: - data = o['fn'](*args, **kwargs) + data = fn(*args, **kwargs) return data def __repr__(self): @@ -522,10 +535,15 @@ def __repr__(self): fn_name = self._unary_funcs[fn] format_string = '{fn}' + prev else: - fn_name = fn.__name__ + if isinstance(fn, basestring): + fn_name = fn + else: + fn_name = fn.__name__ if fn in self._builtin_funcs: fn_name = self._builtin_funcs[fn] format_string = '{fn}'+prev + elif isinstance(fn, basestring): + format_string = prev+').{fn}(' elif fn in self._numpy_funcs: fn_name = self._numpy_funcs[fn] format_string = prev+').{fn}(' From b2f60514ae6e585766c01685be8dc36ca2edfa74 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 17:44:31 +0100 Subject: [PATCH 32/49] Allow applying transform method to indexed datastructure --- holoviews/core/data/__init__.py | 8 +++++++- holoviews/core/data/xarray.py | 21 ++++++++++++++++----- holoviews/selection.py | 7 ++++--- holoviews/util/transform.py | 18 ++++++++++-------- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 8458212c79..f43f9c93d7 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -937,19 +937,25 @@ def transform(self, *args, **kwargs): args: Specify the output arguments and transforms as a tuple of dimension specs and dim transforms drop (bool): Whether to drop all variables not part of the transform + keep_index (bool): Whether to keep indexes + Whether to apply transform on datastructure with + index, e.g. pandas.Series or xarray.DataArray kwargs: Specify new dimensions in the form new_dim=dim_transform Returns: Transformed dataset with new dimensions """ drop = kwargs.pop('drop', False) + keep_index = kwargs.pop('keep_index', False) transforms = OrderedDict() for s, transform in list(args)+list(kwargs.items()): transforms[util.wrap_tuple(s)] = transform new_data = OrderedDict() for signature, transform in transforms.items(): - applied = transform.apply(self, compute=False, keep_index=True) + applied = transform.apply( + self, compute=False, keep_index=keep_index + ) if len(signature) == 1: new_data[signature[0]] = applied else: diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 264af0c257..3becf6d26c 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -353,7 +353,9 @@ def values(cls, dataset, dim, expanded=True, flat=True, compute=True, keep_index if packed: data = dataset.data.data[..., dataset.vdims.index(dim)] else: - data = dataset.data[dim.name].data + data = dataset.data[dim.name] + if not keep_index: + data = data.data irregular = cls.irregular(dataset, dim) if dim in dataset.kdims else False irregular_kdims = [d for d in dataset.kdims if cls.irregular(dataset, d)] if irregular_kdims: @@ -371,13 +373,16 @@ def values(cls, dataset, dim, expanded=True, flat=True, compute=True, keep_index if is_cupy(data): import cupy data = cupy.asnumpy(data) - data = cls.canonicalize(dataset, data, data_coords=data_coords, - virtual_coords=virtual_coords) - return data.T.flatten() if flat else data + if not keep_index: + data = cls.canonicalize(dataset, data, data_coords=data_coords, + virtual_coords=virtual_coords) + return data.T.flatten() if flat and not keep_index else data elif expanded: data = cls.coords(dataset, dim.name, expanded=True) return data.T.flatten() if flat else data else: + if keep_index: + return dataset[dim.name] return cls.coords(dataset, dim.name, ordered=True) @@ -602,11 +607,14 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): @classmethod def assign(cls, dataset, new_data): + import xarray as xr data = dataset.data coords = OrderedDict() for k, v in new_data.items(): if k not in dataset.kdims: continue + elif isinstance(v, xr.DataArray): + coords[k] = v.rename(**{v.name: k}) coord_vals = cls.coords(dataset, k) if not coord_vals.ndim > 1 and np.all(coord_vals[1:] < coord_vals[:-1]): v = v[::-1] @@ -618,7 +626,10 @@ def assign(cls, dataset, new_data): for k, v in new_data.items(): if k in dataset.kdims: continue - vars[k] = (dims, cls.canonicalize(dataset, v, data_coords=dims)) + if isinstance(v, xr.DataArray): + vars[k] = v + else: + vars[k] = (dims, cls.canonicalize(dataset, v, data_coords=dims)) if vars: data = data.assign(vars) return data diff --git a/holoviews/selection.py b/holoviews/selection.py index b34968d43a..bb637f92b7 100644 --- a/holoviews/selection.py +++ b/holoviews/selection.py @@ -477,13 +477,14 @@ def _select(element, selection_expr): dataset = element.dataset try: if dataset.interface.gridded: - mask = selection_expr.apply(dataset, expanded=True, flat=False) + mask = selection_expr.apply(dataset, expanded=True, flat=False, strict=True) selection = dataset.clone(dataset.interface.mask(dataset, ~mask)) elif isinstance(element, (Curve, Spread)) and hasattr(dataset.interface, 'mask'): - mask = selection_expr.apply(dataset) + mask = selection_expr.apply(dataset, compute=False, strict=True) selection = dataset.clone(dataset.interface.mask(dataset, ~mask)) else: - selection = dataset.select(selection_expr=selection_expr) + mask = selection_expr.apply(dataset, compute=False, keep_index=True, strict=True) + selection = dataset.select(selection_mask=mask) element = element.pipeline(selection) except KeyError as e: key_error = str(e).replace('"', '').replace('.', '') diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index 264a4d2374..2e2aded0ac 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -7,7 +7,7 @@ import numpy as np -from ..core.dimension import Dimension +from ..core.dimension import Dimension, dimension_name from ..core.util import basestring, unique_iterator def _maybe_map(numpy_fn): @@ -441,7 +441,7 @@ def applies(self, dataset): return applies def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, - keep_index=False, compute=True): + keep_index=False, compute=True, strict=False): """Evaluates the transform on the supplied dataset. Args: @@ -457,6 +457,7 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, should be preserved in the result. compute: For data types that support lazy evaluation, whether the result should be computed before it is returned. + strict: Whether to strictly check for dimension matches Returns: values: NumPy array computed by evaluating the expression @@ -473,9 +474,10 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, dimension = dataset.nodes.kdims[2] dataset = dataset if dimension in dataset else dataset.nodes + lookup = dimension if strict else dimension.name data = dataset.interface.values( dataset, - dimension, + lookup, expanded=expanded, flat=flat, compute=compute, @@ -483,7 +485,8 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, ) for o in self.ops: args = o['args'] - fn_args = [data] + fn = o['fn'] + fn_args = [] if isinstance(fn, basestring) else [data] for arg in args: if isinstance(arg, dim): arg = arg.apply( @@ -497,16 +500,15 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, ) fn_args.append(arg) args = tuple(fn_args[::-1] if o['reverse'] else fn_args) - eldim = dataset.get_dimension(dimension) - drange = ranges.get(eldim.name, {}) + eldim = dataset.get_dimension(lookup) + drange = ranges.get(dimension_name(lookup), {}) drange = drange.get('combined', drange) - fn = o['fn'] kwargs = o['kwargs'] if (((fn is norm) or (o['fn'] is lognorm)) and drange != {} and not ('min' in kwargs and 'max' in kwargs)): data = fn(data, *drange) elif isinstance(fn, basestring): - data = getattr(data, fn)(*args[1:], **kwargs) + data = getattr(data, fn)(*args, **kwargs) else: data = fn(*args, **kwargs) return data From 99ce8bb274456da682b855e25e4b45477b65ada5 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 17:59:37 +0100 Subject: [PATCH 33/49] Fixed flakes --- holoviews/tests/core/data/testarrayinterface.py | 8 ++++++++ holoviews/tests/core/data/testxarrayinterface.py | 1 + holoviews/util/transform.py | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/holoviews/tests/core/data/testarrayinterface.py b/holoviews/tests/core/data/testarrayinterface.py index ffa8a8ed18..43eefa3a38 100644 --- a/holoviews/tests/core/data/testarrayinterface.py +++ b/holoviews/tests/core/data/testarrayinterface.py @@ -1,3 +1,5 @@ +from unittest import SkipTest + import numpy as np from holoviews.core.data import Dataset @@ -43,3 +45,9 @@ def test_dataset_sort_reverse_hm(self): ds_sorted = Dataset(([2, 2, 1, 1], [2, 1, 2, 1], [0, 2, 1, 3]), kdims=['x', 'y'], vdims=['z']) self.assertEqual(ds.sort(reverse=True), ds_sorted) + + def test_dataset_transform_replace_hm(self): + raise SkipTest("Not supported") + + def test_dataset_transform_add_hm(self): + raise SkipTest("Not supported") diff --git a/holoviews/tests/core/data/testxarrayinterface.py b/holoviews/tests/core/data/testxarrayinterface.py index 292f1b4adc..b37eb36c75 100644 --- a/holoviews/tests/core/data/testxarrayinterface.py +++ b/holoviews/tests/core/data/testxarrayinterface.py @@ -1,4 +1,5 @@ import datetime as dt + from collections import OrderedDict from unittest import SkipTest diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index 2e2aded0ac..985031348d 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -501,7 +501,7 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, fn_args.append(arg) args = tuple(fn_args[::-1] if o['reverse'] else fn_args) eldim = dataset.get_dimension(lookup) - drange = ranges.get(dimension_name(lookup), {}) + drange = ranges.get(eldim.name, {}) drange = drange.get('combined', drange) kwargs = o['kwargs'] if (((fn is norm) or (o['fn'] is lognorm)) and drange != {} and From 6e83b97db10656565e7e2fb04da6df014e70f8d6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 18:14:24 +0100 Subject: [PATCH 34/49] Add support for dropping coords --- holoviews/core/data/__init__.py | 4 +++- holoviews/core/data/xarray.py | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index f43f9c93d7..593da76f0f 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -979,7 +979,9 @@ def transform(self, *args, **kwargs): else: new_data = OrderedDict([(dimension_name(d), values) for d, values in new_data.items()]) data = ds.interface.assign(ds, new_data) - return ds.clone(data, vdims=ds.vdims+new_dims) + data, drop = data if isinstance(data, tuple) else (data, []) + kdims = [kd for kd in self.kdims if kd.name not in drop] + return ds.clone(data, kdims=kdims, vdims=ds.vdims+new_dims) def __len__(self): "Number of values in the Dataset." diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 3becf6d26c..7cd8bd715c 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -609,6 +609,9 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): def assign(cls, dataset, new_data): import xarray as xr data = dataset.data + prev_coords = set.intersection(*[ + set(var.coords) for var in data.data_vars.values() + ]) coords = OrderedDict() for k, v in new_data.items(): if k not in dataset.kdims: @@ -632,7 +635,9 @@ def assign(cls, dataset, new_data): vars[k] = (dims, cls.canonicalize(dataset, v, data_coords=dims)) if vars: data = data.assign(vars) - return data + used_coords = set.intersection(*[set(var.coords) for var in data.data_vars.values()]) + drop_coords = set.symmetric_difference(used_coords, prev_coords) + return data.drop(list(drop_coords)), list(drop_coords) Interface.register(XArrayInterface) From cb634cbcc3c8141e9c5f8078f38a8461870ed471 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 18:20:38 +0100 Subject: [PATCH 35/49] Defer NumPy function calls to method on data --- holoviews/util/transform.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index 985031348d..eb6c82ac7c 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -486,6 +486,9 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, for o in self.ops: args = o['args'] fn = o['fn'] + fn_name = self._numy_funcs.get(fn) + if fn_name and hasattr(data, fn_name): + fn = fn_name fn_args = [] if isinstance(fn, basestring) else [data] for arg in args: if isinstance(arg, dim): From 6221ae678592a0e009ddcbb531e8ebd48f1dad39 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 18:21:46 +0100 Subject: [PATCH 36/49] Fixed flakes --- holoviews/util/transform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index eb6c82ac7c..8e78533794 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -7,7 +7,7 @@ import numpy as np -from ..core.dimension import Dimension, dimension_name +from ..core.dimension import Dimension from ..core.util import basestring, unique_iterator def _maybe_map(numpy_fn): @@ -486,7 +486,7 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, for o in self.ops: args = o['args'] fn = o['fn'] - fn_name = self._numy_funcs.get(fn) + fn_name = self._numpy_funcs.get(fn) if fn_name and hasattr(data, fn_name): fn = fn_name fn_args = [] if isinstance(fn, basestring) else [data] From 680b5f2a38cb13f11a5da0434b3e2e64cdfea71f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 18:50:52 +0100 Subject: [PATCH 37/49] Fixed py2 issue --- holoviews/core/accessors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py index 1c50ab1fab..ec11a48988 100644 --- a/holoviews/core/accessors.py +++ b/holoviews/core/accessors.py @@ -252,7 +252,7 @@ def select(self, **kwargs): """ return self.__call__('select', **kwargs) - def transform(self, *args, drop=False, **kwargs): + def transform(self, *args, **kwargs): """Applies transforms to all Datasets. See :py:meth:`Dataset.transform` and :py:meth:`Apply.__call__` From 315025cb3c681537b100d09846a986ba216f42de Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 19:03:20 +0100 Subject: [PATCH 38/49] Minor fixes for numpy transforms --- holoviews/tests/util/testtransform.py | 4 ++-- holoviews/util/transform.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/holoviews/tests/util/testtransform.py b/holoviews/tests/util/testtransform.py index 76bbe3d68a..1fc8fe1986 100644 --- a/holoviews/tests/util/testtransform.py +++ b/holoviews/tests/util/testtransform.py @@ -209,11 +209,11 @@ def test_sum_transform(self): self.check_apply(expr, self.linear_floats.sum()) def test_std_transform(self): - expr = dim('float').std() + expr = dim('float').std(ddof=0) self.check_apply(expr, self.linear_floats.std(ddof=0)) def test_var_transform(self): - expr = dim('float').var() + expr = dim('float').var(ddof=0) self.check_apply(expr, self.linear_floats.var(ddof=0)) def test_log_transform(self): diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index 8e78533794..00636b8bd3 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -486,8 +486,11 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, for o in self.ops: args = o['args'] fn = o['fn'] + kwargs = dict(o['kwargs']) fn_name = self._numpy_funcs.get(fn) if fn_name and hasattr(data, fn_name): + if 'axis' not in kwargs and not isinstance(fn, np.ufunc): + kwargs['axis'] = None fn = fn_name fn_args = [] if isinstance(fn, basestring) else [data] for arg in args: @@ -506,7 +509,6 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, eldim = dataset.get_dimension(lookup) drange = ranges.get(eldim.name, {}) drange = drange.get('combined', drange) - kwargs = o['kwargs'] if (((fn is norm) or (o['fn'] is lognorm)) and drange != {} and not ('min' in kwargs and 'max' in kwargs)): data = fn(data, *drange) From 995d0d46ccac156ed22180fcf0b96b45c17a9b8f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 19:08:29 +0100 Subject: [PATCH 39/49] Better error handling on transforms --- holoviews/util/transform.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index 00636b8bd3..af9a25255a 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -513,7 +513,14 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, not ('min' in kwargs and 'max' in kwargs)): data = fn(data, *drange) elif isinstance(fn, basestring): - data = getattr(data, fn)(*args, **kwargs) + method = getattr(data, fn, None) + if method is None: + raise AttributeError( + "%r could not be applied to '%r', '%s' method " + "does not exist on %s type." + % (self, dataset, fn, type(data).__name__) + ) + data = method(*args, **kwargs) else: data = fn(*args, **kwargs) return data @@ -529,7 +536,7 @@ def __repr__(self): ufunc = isinstance(fn, np.ufunc) args = ', '.join([repr(r) for r in o['args']]) if o['args'] else '' kwargs = sorted(o['kwargs'].items(), key=operator.itemgetter(0)) - kwargs = '%s' % ', '.join(['%s=%s' % item for item in kwargs]) if kwargs else '' + kwargs = '%s' % ', '.join(['%s=%r' % item for item in kwargs]) if kwargs else '' if fn in self._binary_funcs: fn_name = self._binary_funcs[o['fn']] if o['reverse']: From 39d19803da0070a75736bcda84a71b294e2d21db Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 19:12:07 +0100 Subject: [PATCH 40/49] Fixed flakes --- holoviews/core/accessors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py index ec11a48988..c7e8457093 100644 --- a/holoviews/core/accessors.py +++ b/holoviews/core/accessors.py @@ -260,7 +260,7 @@ def transform(self, *args, **kwargs): """ kwargs['_method_args'] = args kwargs['per_element'] = True - return self.__call__('transform', drop=drop, **kwargs) + return self.__call__('transform', **kwargs) @add_metaclass(AccessorPipelineMeta) class Redim(object): From a79cc5c3a41b5d23b96c9bdfcaa1be01e6a75261 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 6 Mar 2020 20:40:09 +0100 Subject: [PATCH 41/49] Resolve parameters in dim expressions --- holoviews/core/accessors.py | 17 +++++++++++++ holoviews/core/util.py | 49 +++++++++++++++++++++++-------------- holoviews/streams.py | 4 +-- holoviews/util/transform.py | 17 ++++++++++++- 4 files changed, 66 insertions(+), 21 deletions(-) diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py index c7e8457093..7989c9a32f 100644 --- a/holoviews/core/accessors.py +++ b/holoviews/core/accessors.py @@ -258,10 +258,27 @@ def transform(self, *args, **kwargs): See :py:meth:`Dataset.transform` and :py:meth:`Apply.__call__` for more information. """ + from ..streams import Params + streams = [] + params = {} + for _, arg in list(args)+list(kwargs.items()): + for op in arg.ops: + op_args = list(op['args'])+list(op['kwargs'].items()) + for op_arg in op_args: + if 'panel' in sys.modules: + from panel.widgets.base import Widget + if isinstance(op_arg, Widget): + op_arg = op_arg.param.value + if (isinstance(op_arg, param.Parameter) and + isinstance(op_arg.owner, param.Parameterized)): + params[op_arg.name+str(id(op))] = op_arg + streams += Params.from_params(params, watch_only=True) kwargs['_method_args'] = args kwargs['per_element'] = True + kwargs['streams'] = streams return self.__call__('transform', **kwargs) + @add_metaclass(AccessorPipelineMeta) class Redim(object): """ diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 4d7c2ab701..997c691ec9 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -1504,6 +1504,35 @@ def is_param_method(obj, has_deps=False): return parameterized +def resolve_dependent_value(value): + """Resolves parameter dependencies on the supplied value + + Resolves parameter values, Parameterized instance methods and + parameterized functions with dependencies on the supplied value. + + Args: + value: A value which will be resolved + + Returns: + A new dictionary with where any parameter dependencies have been + resolved. + """ + if 'panel' in sys.modules: + from panel.widgets.base import Widget + if isinstance(value, Widget): + value = value.param.value + if is_param_method(value, has_deps=True): + value = value() + elif isinstance(value, param.Parameter) and isinstance(value.owner, param.Parameterized): + value = getattr(value.owner, value.name) + elif isinstance(value, FunctionType) and hasattr(value, '_dinfo'): + deps = value._dinfo + args = (getattr(p.owner, p.name) for p in deps.get('dependencies', [])) + kwargs = {k: getattr(p.owner, p.name) for k, p in deps.get('kw', {}).items()} + value = value(*args, **kwargs) + return value + + def resolve_dependent_kwargs(kwargs): """Resolves parameter dependencies in the supplied dictionary @@ -1518,23 +1547,7 @@ def resolve_dependent_kwargs(kwargs): A new dictionary with where any parameter dependencies have been resolved. """ - resolved = {} - for k, v in kwargs.items(): - if 'panel' in sys.modules: - from panel.widgets.base import Widget - if isinstance(v, Widget): - v = v.param.value - if is_param_method(v, has_deps=True): - v = v() - elif isinstance(v, param.Parameter) and isinstance(v.owner, param.Parameterized): - v = getattr(v.owner, v.name) - elif isinstance(v, FunctionType) and hasattr(v, '_dinfo'): - deps = v._dinfo - args = (getattr(p.owner, p.name) for p in deps.get('dependencies', [])) - kwargs = {k: getattr(p.owner, p.name) for k, p in deps.get('kw', {}).items()} - v = v(*args, **kwargs) - resolved[k] = v - return resolved + return {k: resolve_dependent_value(v) for k, v in kwargs.items()} @contextmanager @@ -1641,7 +1654,7 @@ def stream_parameters(streams, no_duplicates=True, exclude=['name']): for c in clashes: if c in s.contents or (not s.contents and isinstance(s.hashkey, dict) and c in s.hashkey): clash_streams.append(s) - if clashes: + if [c for c in clashes if c != '_memoize_key']: clashing = ', '.join([repr(c) for c in clash_streams[:-1]]) raise Exception('The supplied stream objects %s and %s ' 'clash on the following parameters: %r' diff --git a/holoviews/streams.py b/holoviews/streams.py index 9b061bb641..25cd54c294 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -681,7 +681,7 @@ def __init__(self, parameterized=None, parameters=None, watch=True, watch_only=F group[0].owner.param.watch(self._watcher, [p.name for p in group]) @classmethod - def from_params(cls, params): + def from_params(cls, params, **kwargs): """Returns Params streams given a dictionary of parameters Args: @@ -699,7 +699,7 @@ def from_params(cls, params): continue names = [p.name for _, p in group] rename = {p.name: n for n, p in group} - streams.append(cls(inst, names, rename=rename)) + streams.append(cls(inst, names, rename=rename, **kwargs)) return streams def _validate_rename(self, mapping): diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index af9a25255a..ef0d8ee0fb 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -8,7 +8,7 @@ import numpy as np from ..core.dimension import Dimension -from ..core.util import basestring, unique_iterator +from ..core.util import basestring, resolve_dependent_value, unique_iterator def _maybe_map(numpy_fn): def fn(values, *args, **kwargs): @@ -504,8 +504,23 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, keep_index, compute, ) + arg = resolve_dependent_value(arg) fn_args.append(arg) + fn_kwargs = {} + for k, v in kwargs.items(): + if isinstance(v, dim): + v = v.apply( + dataset, + flat, + expanded, + ranges, + all_values, + keep_index, + compute, + ) + fn_kwargs[k] = resolve_dependent_value(v) args = tuple(fn_args[::-1] if o['reverse'] else fn_args) + kwargs = dict(fn_kwargs) eldim = dataset.get_dimension(lookup) drange = ranges.get(eldim.name, {}) drange = drange.get('combined', drange) From c10f014095a162746b4ff57218805f6a244aa22e Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 7 Mar 2020 01:59:11 +0100 Subject: [PATCH 42/49] Implement Dataset.__new__ to allow casting DynamicMaps --- holoviews/core/data/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 593da76f0f..4659915992 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -281,6 +281,12 @@ class Dataset(Element): _vdim_reductions = {} _kdim_reductions = {} + def __new__(cls, data, kdims=None, vdims=None, **kwargs): + if isinstance(data, DynamicMap): + return data.apply(cls, per_element=True, kdims=kdims, vdims=vdims, **kwargs) + else: + return super(Dataset, cls).__new__(cls) + def __init__(self, data, kdims=None, vdims=None, **kwargs): from ...operation.element import ( chain as chain_op, factory From 584d46a4adf47527a0d64cb9904fb339caed1d33 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 7 Mar 2020 02:20:40 +0100 Subject: [PATCH 43/49] Generalized transform watching to apply.opts --- holoviews/core/accessors.py | 26 +++++++++++++------------- holoviews/util/transform.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py index 7989c9a32f..921c688d09 100644 --- a/holoviews/core/accessors.py +++ b/holoviews/core/accessors.py @@ -221,6 +221,14 @@ def opts(self, *args, **kwargs): See :py:meth:`Dimensioned.opts` and :py:meth:`Apply.__call__` for more information. """ + from ..util.transform import dim + from ..streams import Params + params = {} + for arg in kwargs.values(): + if isinstance(arg, dim): + params.update(arg.params) + streams = Params.from_params(params, watch_only=True) + kwargs['streams'] = kwargs.get('streams', []) + streams kwargs['_method_args'] = args return self.__call__('opts', **kwargs) @@ -258,24 +266,16 @@ def transform(self, *args, **kwargs): See :py:meth:`Dataset.transform` and :py:meth:`Apply.__call__` for more information. """ + from ..util.transform import dim from ..streams import Params - streams = [] params = {} for _, arg in list(args)+list(kwargs.items()): - for op in arg.ops: - op_args = list(op['args'])+list(op['kwargs'].items()) - for op_arg in op_args: - if 'panel' in sys.modules: - from panel.widgets.base import Widget - if isinstance(op_arg, Widget): - op_arg = op_arg.param.value - if (isinstance(op_arg, param.Parameter) and - isinstance(op_arg.owner, param.Parameterized)): - params[op_arg.name+str(id(op))] = op_arg - streams += Params.from_params(params, watch_only=True) + if isinstance(arg, dim): + params.update(arg.params) + streams = Params.from_params(params, watch_only=True) + kwargs['streams'] = kwargs.get('streams', []) + streams kwargs['_method_args'] = args kwargs['per_element'] = True - kwargs['streams'] = streams return self.__call__('transform', **kwargs) diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index ef0d8ee0fb..e3fc9929be 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -1,11 +1,13 @@ from __future__ import division import operator +import sys from functools import partial from types import BuiltinFunctionType, BuiltinMethodType, FunctionType, MethodType import numpy as np +import param from ..core.dimension import Dimension from ..core.util import basestring, resolve_dependent_value, unique_iterator @@ -274,6 +276,21 @@ def __getattr__(self, attr): return self.__dict__[attr] return partial(self.method, attr) + @property + def params(self): + params = {} + for op in self.ops: + op_args = list(op['args'])+list(op['kwargs'].items()) + for op_arg in op_args: + if 'panel' in sys.modules: + from panel.widgets.base import Widget + if isinstance(op_arg, Widget): + op_arg = op_arg.param.value + if (isinstance(op_arg, param.Parameter) and + isinstance(op_arg.owner, param.Parameterized)): + params[op_arg.name+str(id(op))] = op_arg + return params + def method(self, method, *args, **kwargs): return dim(self, method, *args, **kwargs) From 3497917c79ec20e432fee70032b8c4b96af18cd9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 8 Mar 2020 11:09:04 +0100 Subject: [PATCH 44/49] Apply suggestions from code review Co-Authored-By: James A. Bednar --- holoviews/core/data/__init__.py | 8 ++++++-- holoviews/core/util.py | 2 +- holoviews/util/transform.py | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 4659915992..ded9e35375 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -282,6 +282,10 @@ class Dataset(Element): _kdim_reductions = {} def __new__(cls, data, kdims=None, vdims=None, **kwargs): + """ + Allows casting a DynamicMap to an Element class like hv.Curve, by applying the + class to each underlying element. + """ if isinstance(data, DynamicMap): return data.apply(cls, per_element=True, kdims=kdims, vdims=vdims, **kwargs) else: @@ -456,7 +460,7 @@ def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs): Args: dimension: Dimension or dimension spec to add - dim_pos (int) Integer index to insert dimension at + dim_pos (int): Integer index to insert dimension at dim_val (scalar or ndarray): Dimension value(s) to add vdim: Disabled, this type does not have value dimensions **kwargs: Keyword arguments passed to the cloned element @@ -803,7 +807,7 @@ def aggregate(self, dimensions=None, function=None, spreadfn=None, **kwargs): """Aggregates data on the supplied dimensions. Aggregates over the supplied key dimensions with the defined - function or dim_transform specified as tuple of the transformed + function or dim_transform specified as a tuple of the transformed dimension name and dim transform. Args: diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 997c691ec9..dc4d164a48 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -1514,7 +1514,7 @@ def resolve_dependent_value(value): value: A value which will be resolved Returns: - A new dictionary with where any parameter dependencies have been + A new dictionary where any parameter dependencies have been resolved. """ if 'panel' in sys.modules: diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index e3fc9929be..540d9a1cb8 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -475,6 +475,7 @@ def apply(self, dataset, flat=False, expanded=None, ranges={}, all_values=False, compute: For data types that support lazy evaluation, whether the result should be computed before it is returned. strict: Whether to strictly check for dimension matches + (if False, counts any dimensions with matching names as the same) Returns: values: NumPy array computed by evaluating the expression From 0e9e28d84cc4b54aa6b1c9e1e8f3461eca8cb693 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 8 Mar 2020 11:15:21 +0100 Subject: [PATCH 45/49] Made __new__ backward compatible --- holoviews/core/data/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index ded9e35375..2db371f88c 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -281,7 +281,7 @@ class Dataset(Element): _vdim_reductions = {} _kdim_reductions = {} - def __new__(cls, data, kdims=None, vdims=None, **kwargs): + def __new__(cls, data=None, kdims=None, vdims=None, **kwargs): """ Allows casting a DynamicMap to an Element class like hv.Curve, by applying the class to each underlying element. From f0fad5775d605f6a04aaef93375482d3d341bb71 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 8 Mar 2020 11:17:38 +0100 Subject: [PATCH 46/49] Fix issue with BaseShape.__new__ --- holoviews/element/path.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/holoviews/element/path.py b/holoviews/element/path.py index ec19381090..a40bffc228 100644 --- a/holoviews/element/path.py +++ b/holoviews/element/path.py @@ -369,6 +369,9 @@ class BaseShape(Path): __abstract = True + def __new__(cls, *args, **kwargs): + return super(Dataset, cls).__new__(cls) + def __init__(self, **params): super(BaseShape, self).__init__([], **params) self.interface = MultiInterface From c29226705e5f8ce62ee46b4a77f7365bd1a83448 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 8 Mar 2020 19:25:55 +0100 Subject: [PATCH 47/49] Pass assign_coords as kwargs --- holoviews/core/data/xarray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 7cd8bd715c..58ed12d3fd 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -623,7 +623,7 @@ def assign(cls, dataset, new_data): v = v[::-1] coords[k] = (k, v) if coords: - data = data.assign_coords(coords) + data = data.assign_coords(**xcoords) dims = tuple(kd.name for kd in dataset.kdims[::-1]) vars = OrderedDict() for k, v in new_data.items(): From b0b252c28c657fdef2559d3e2b07a3077f3a9537 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 8 Mar 2020 19:25:55 +0100 Subject: [PATCH 48/49] Pass assign_coords as kwargs --- holoviews/core/data/xarray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 7cd8bd715c..4105f91e4e 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -623,7 +623,7 @@ def assign(cls, dataset, new_data): v = v[::-1] coords[k] = (k, v) if coords: - data = data.assign_coords(coords) + data = data.assign_coords(**coords) dims = tuple(kd.name for kd in dataset.kdims[::-1]) vars = OrderedDict() for k, v in new_data.items(): From a3376e5ef595f446baad053b8d175ef769cd053b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 9 Mar 2020 18:18:14 +0100 Subject: [PATCH 49/49] Fixes for xarray assign --- holoviews/core/data/xarray.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 4105f91e4e..4a02cf863f 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -382,7 +382,7 @@ def values(cls, dataset, dim, expanded=True, flat=True, compute=True, keep_index return data.T.flatten() if flat else data else: if keep_index: - return dataset[dim.name] + return dataset.data[dim.name] return cls.coords(dataset, dim.name, ordered=True) @@ -618,6 +618,7 @@ def assign(cls, dataset, new_data): continue elif isinstance(v, xr.DataArray): coords[k] = v.rename(**{v.name: k}) + continue coord_vals = cls.coords(dataset, k) if not coord_vals.ndim > 1 and np.all(coord_vals[1:] < coord_vals[:-1]): v = v[::-1]