From c6b7ea433efcbf82b4753108b890db1fb8fde53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 13 Dec 2024 15:05:33 +0100 Subject: [PATCH 1/9] Hide direct pandas import --- holoviews/core/data/cudf.py | 6 ++++-- holoviews/core/data/spatialpandas.py | 4 +++- holoviews/core/data/xarray.py | 3 ++- holoviews/element/graphs.py | 4 +++- holoviews/element/selection.py | 2 +- holoviews/operation/timeseries.py | 2 +- holoviews/plotting/bokeh/annotation.py | 3 ++- holoviews/plotting/bokeh/util.py | 2 +- holoviews/streams.py | 5 ++++- holoviews/util/transform.py | 3 ++- 10 files changed, 23 insertions(+), 11 deletions(-) diff --git a/holoviews/core/data/cudf.py b/holoviews/core/data/cudf.py index f316e5e6e4..2e886b4304 100644 --- a/holoviews/core/data/cudf.py +++ b/holoviews/core/data/cudf.py @@ -3,8 +3,6 @@ from itertools import product import numpy as np -import pandas as pd -from pandas.api.types import is_numeric_dtype from .. import util from ..dimension import dimension_name @@ -49,6 +47,7 @@ def applies(cls, obj): @classmethod def init(cls, eltype, data, kdims, vdims): import cudf + import pandas as pd element_params = eltype.param.objects() kdim_param = element_params['kdims'] @@ -276,6 +275,9 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): @classmethod def aggregate(cls, dataset, dimensions, function, **kwargs): + import pandas as pd + from pandas.api.types import is_numeric_dtype + data = dataset.data cols = [d.name for d in dataset.kdims if d in dimensions] vdims = dataset.dimensions('value', label='name') diff --git a/holoviews/core/data/spatialpandas.py b/holoviews/core/data/spatialpandas.py index 5a8af8b8cd..9e1102ddf1 100644 --- a/holoviews/core/data/spatialpandas.py +++ b/holoviews/core/data/spatialpandas.py @@ -2,7 +2,6 @@ from collections import defaultdict import numpy as np -import pandas as pd from ..dimension import dimension_name from ..util import isscalar, unique_array, unique_iterator @@ -98,6 +97,8 @@ def init(cls, eltype, data, kdims, vdims): elif 'geometry' not in data: cls.geo_column(data) + import pandas as pd + index_names = data.index.names if isinstance(data, pd.DataFrame) else [data.index.name] if index_names == [None]: index_names = ['index'] @@ -865,6 +866,7 @@ def from_multi(eltype, data, kdims, vdims): Returns: A GeoDataFrame containing in the list based format. """ + import pandas as pd from spatialpandas import GeoDataFrame xname, yname = (kd.name for kd in kdims[:2]) diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 38d25f9649..99f2bedffa 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -2,7 +2,6 @@ import types import numpy as np -import pandas as pd from .. import util from ..dimension import Dimension, asdim, dimension_name @@ -654,6 +653,8 @@ def dframe(cls, dataset, dimensions): @classmethod def sample(cls, dataset, samples=None): + import pandas as pd + if samples is None: samples = [] names = [kd.name for kd in dataset.kdims] diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index b25f9c48f9..88e8dc37d7 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -2,7 +2,6 @@ from types import FunctionType import numpy as np -import pandas as pd import param from ..core import Dataset, Dimension, Element2D @@ -69,6 +68,7 @@ def _process(self, element, key=None): target = element.dimension_values(1, expanded=False) nodes = np.unique(np.concatenate([source, target])) if self.p.layout: + import pandas as pd df = pd.DataFrame({'index': nodes}) nodes = self.p.layout(df, element.dframe(), **self.p.kwargs) nodes = nodes[['x', 'y', 'index']] @@ -165,6 +165,8 @@ def redim(self): return RedimGraph(self, mode='dataset') def _add_node_info(self, node_info): + import pandas as pd + nodes = self.nodes.clone(datatype=['pandas', 'dictionary']) if isinstance(node_info, self.node_type): nodes = nodes.redim(**dict(zip(nodes.dimensions('key', label=True), diff --git a/holoviews/element/selection.py b/holoviews/element/selection.py index 349c3ee667..962084e8e4 100644 --- a/holoviews/element/selection.py +++ b/holoviews/element/selection.py @@ -7,7 +7,6 @@ from importlib.util import find_spec import numpy as np -import pandas as pd from ..core import Dataset, NdOverlay, util from ..streams import Lasso, Selection1D, SelectionXY @@ -115,6 +114,7 @@ def _cuspatial_new(xvals, yvals, geometry): def spatial_select_columnar(xvals, yvals, geometry, geom_method=None): + import pandas as pd if 'cudf' in sys.modules: import cudf import cupy as cp diff --git a/holoviews/operation/timeseries.py b/holoviews/operation/timeseries.py index f0cb099c9a..3897dad5c8 100644 --- a/holoviews/operation/timeseries.py +++ b/holoviews/operation/timeseries.py @@ -1,5 +1,4 @@ import numpy as np -import pandas as pd import param from ..core import Element, Operation @@ -110,6 +109,7 @@ class rolling_outlier_std(Operation, RollingBase): Minimum sigma before a value is considered an outlier.""") def _process_layer(self, element, key=None): + import pandas as pd ys = element.dimension_values(1) # Calculate the variation in the distribution of the residual diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index bf32bb70ed..2e92485ea0 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -3,7 +3,6 @@ from html import escape import numpy as np -import pandas as pd import param from bokeh.models import Arrow, BoxAnnotation, NormalHead, Slope, Span, TeeHead from bokeh.transform import dodge @@ -73,6 +72,8 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): return figure def get_extents(self, element, ranges=None, range_type='combined', **kwargs): + import pandas as pd + extents = super().get_extents(element, ranges, range_type) if isinstance(element, HLines): extents = np.nan, extents[0], np.nan, extents[2] diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index 685a6e3bcd..5ed47eaa23 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -8,7 +8,6 @@ import bokeh import numpy as np -import pandas as pd from bokeh.core.json_encoder import serialize_json # noqa (API import) from bokeh.core.property.datetime import Datetime from bokeh.core.validation import silence @@ -980,6 +979,7 @@ def date_to_integer(date): Returns: Milliseconds since 1970-01-01 00:00:00 """ + import pandas as pd if isinstance(date, pd.Timestamp): try: date = date.to_datetime64() diff --git a/holoviews/streams.py b/holoviews/streams.py index 158bf91401..3a258aa11f 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -13,7 +13,6 @@ from types import FunctionType import numpy as np -import pandas as pd import param from .core import util @@ -542,6 +541,7 @@ class Buffer(Pipe): Arbitrary data being streamed to a DynamicMap callback.""") def __init__(self, data, length=1000, index=True, following=True, **params): + import pandas as pd if isinstance(data, pd.DataFrame): example = data elif isinstance(data, np.ndarray): @@ -587,6 +587,7 @@ def __init__(self, data, length=1000, index=True, following=True, **params): def verify(self, x): """ Verify consistency of dataframes that pass through this stream """ + import pandas as pd if type(x) != type(self.data): # noqa: E721 raise TypeError(f"Input expected to be of type {type(self.data).__name__}, got {type(x).__name__}.") elif isinstance(x, np.ndarray): @@ -606,6 +607,7 @@ def verify(self, x): def clear(self): + import pandas as pd "Clears the data in the stream" if isinstance(self.data, np.ndarray): data = self.data[:, :0] @@ -623,6 +625,7 @@ def _concat(self, data): Concatenate and slice the accepted data types to the defined length. """ + import pandas as pd if isinstance(data, np.ndarray): data_length = len(data) if not self.length: diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index 8c95d3d4ae..8915d57315 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -2,7 +2,6 @@ from types import BuiltinFunctionType, BuiltinMethodType, FunctionType, MethodType import numpy as np -import pandas as pd import param from ..core.data import PandasInterface @@ -82,6 +81,7 @@ def __getitem__(self, index): return dim(self.expr, self) def __call__(self, values): + import pandas as pd if isinstance(values, (pd.Series, pd.DataFrame)): return values.iloc[resolve_dependent_value(self.index)] else: @@ -867,6 +867,7 @@ class df_dim(dim): _accessor = 'pd' def __init__(self, obj, *args, **kwargs): + import pandas as pd super().__init__(obj, *args, **kwargs) self._ns = pd.Series From 344a8bab1018d76d0a7e4311c57005bed1e96d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 13 Dec 2024 15:17:37 +0100 Subject: [PATCH 2/9] moving day --- holoviews/core/data/ibis.py | 2 +- holoviews/core/{util.py => util/__init__.py} | 67 ++++---------------- holoviews/core/util/types.py | 27 ++++++++ holoviews/core/util/versions.py | 27 ++++++++ 4 files changed, 66 insertions(+), 57 deletions(-) rename holoviews/core/{util.py => util/__init__.py} (97%) create mode 100644 holoviews/core/util/types.py create mode 100644 holoviews/core/util/versions.py diff --git a/holoviews/core/data/ibis.py b/holoviews/core/data/ibis.py index 06011025b0..2b48ac7d1e 100644 --- a/holoviews/core/data/ibis.py +++ b/holoviews/core/data/ibis.py @@ -9,7 +9,7 @@ from .interface import DataError, Interface from .util import cached -IBIS_VERSION = util._no_import_version("ibis-framework") +IBIS_VERSION = util.versions._no_import_version("ibis-framework") IBIS_GE_4_0_0 = IBIS_VERSION >= (4, 0, 0) IBIS_GE_5_0_0 = IBIS_VERSION >= (5, 0, 0) IBIS_GE_9_5_0 = IBIS_VERSION >= (9, 5, 0) diff --git a/holoviews/core/util.py b/holoviews/core/util/__init__.py similarity index 97% rename from holoviews/core/util.py rename to holoviews/core/util/__init__.py index 05daabb7aa..72e0b833ee 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util/__init__.py @@ -18,17 +18,14 @@ from collections import defaultdict, namedtuple from contextlib import contextmanager from functools import partial -from importlib.metadata import PackageNotFoundError, version from threading import Event, Thread from types import FunctionType import numpy as np import pandas as pd import param -from packaging.version import Version -from pandas.core.arrays.masked import BaseMaskedArray -from pandas.core.dtypes.dtypes import DatetimeTZDtype -from pandas.core.dtypes.generic import ABCExtensionArray, ABCIndex, ABCSeries + +from . import versions # noqa: F401 # Python 2 builtins basestring = str @@ -38,41 +35,16 @@ get_keywords = operator.attrgetter('varkw') -# Versions -NUMPY_VERSION = Version(np.__version__).release -PARAM_VERSION = Version(param.__version__).release -PANDAS_VERSION = Version(pd.__version__).release - -NUMPY_GE_2_0_0 = NUMPY_VERSION >= (2, 0, 0) -PANDAS_GE_2_1_0 = PANDAS_VERSION >= (2, 1, 0) -PANDAS_GE_2_2_0 = PANDAS_VERSION >= (2, 2, 0) - -# Types -generator_types = (zip, range, types.GeneratorType) -pandas_datetime_types = (pd.Timestamp, DatetimeTZDtype, pd.Period) -pandas_timedelta_types = (pd.Timedelta,) -datetime_types = (np.datetime64, dt.datetime, dt.date, dt.time, *pandas_datetime_types) -timedelta_types = (np.timedelta64, dt.timedelta, *pandas_timedelta_types) -arraylike_types = (np.ndarray, ABCSeries, ABCIndex, ABCExtensionArray) -masked_types = (BaseMaskedArray,) - -try: - import cftime - cftime_types = (cftime.datetime,) - datetime_types += cftime_types -except ImportError: - cftime_types = () -_STANDARD_CALENDARS = {'standard', 'gregorian', 'proleptic_gregorian'} - anonymous_dimension_label = '_' # Argspec was removed in Python 3.11 ArgSpec = namedtuple('ArgSpec', 'args varargs keywords defaults') -_NP_SIZE_LARGE = 1_000_000 -_NP_SAMPLE_SIZE = 1_000_000 -_PANDAS_ROWS_LARGE = 1_000_000 -_PANDAS_SAMPLE_SIZE = 1_000_000 +_STANDARD_CALENDARS = {'standard', 'gregorian', 'proleptic_gregorian'} +_ARRAY_SIZE_LARGE = 1_000_000 +_ARRAY_SAMPLE_SIZE = 1_000_000 +_DATAFRAME_ROWS_LARGE = 1_000_000 +_DATAFRAME_SAMPLE_SIZE = 1_000_000 # To avoid pandas warning about using DataFrameGroupBy.function # introduced in Pandas 2.1. @@ -107,23 +79,6 @@ np.nancumsum: "cumsum", } - -class VersionError(Exception): - "Raised when there is a library version mismatch." - def __init__(self, msg, version=None, min_version=None, **kwargs): - self.version = version - self.min_version = min_version - super().__init__(msg, **kwargs) - - -def _no_import_version(name) -> tuple[int, int, int]: - """ Get version number without importing the library """ - try: - return Version(version(name)).release - except PackageNotFoundError: - return (0, 0, 0) - - class Config(param.ParameterizedFunction): """ Set of boolean configuration values to change HoloViews' global @@ -207,14 +162,14 @@ def default(self, obj): h = hashlib.new("md5") for s in obj.shape: h.update(_int_to_bytes(s)) - if obj.size >= _NP_SIZE_LARGE: + if obj.size >= _ARRAY_SIZE_LARGE: state = np.random.RandomState(0) - obj = state.choice(obj.flat, size=_NP_SAMPLE_SIZE) + obj = state.choice(obj.flat, size=_ARRAY_SAMPLE_SIZE) h.update(obj.tobytes()) return h.hexdigest() if isinstance(obj, (pd.Series, pd.DataFrame)): - if len(obj) > _PANDAS_ROWS_LARGE: - obj = obj.sample(n=_PANDAS_SAMPLE_SIZE, random_state=0) + if len(obj) > _DATAFRAME_ROWS_LARGE: + obj = obj.sample(n=_DATAFRAME_SAMPLE_SIZE, random_state=0) try: pd_values = list(pd.util.hash_pandas_object(obj, index=True).values) except TypeError: diff --git a/holoviews/core/util/types.py b/holoviews/core/util/types.py new file mode 100644 index 0000000000..59339978ce --- /dev/null +++ b/holoviews/core/util/types.py @@ -0,0 +1,27 @@ +import types +from datetime import datetime as dt + + +import numpy as np +import pandas as pd +from pandas.core.arrays.masked import BaseMaskedArray +from pandas.core.dtypes.dtypes import DatetimeTZDtype +from pandas.core.dtypes.generic import ABCExtensionArray, ABCIndex, ABCSeries + + +# Types +generator_types = (zip, range, types.GeneratorType) +pandas_datetime_types = (pd.Timestamp, DatetimeTZDtype, pd.Period) +pandas_timedelta_types = (pd.Timedelta,) +datetime_types = (np.datetime64, dt.datetime, dt.date, dt.time, *pandas_datetime_types) +timedelta_types = (np.timedelta64, dt.timedelta, *pandas_timedelta_types) +arraylike_types = (np.ndarray, ABCSeries, ABCIndex, ABCExtensionArray) +masked_types = (BaseMaskedArray,) + +try: + import cftime + cftime_types = (cftime.datetime,) + datetime_types += cftime_types +except ImportError: + cftime_types = () +_STANDARD_CALENDARS = {'standard', 'gregorian', 'proleptic_gregorian'} diff --git a/holoviews/core/util/versions.py b/holoviews/core/util/versions.py new file mode 100644 index 0000000000..b866ce5940 --- /dev/null +++ b/holoviews/core/util/versions.py @@ -0,0 +1,27 @@ +from importlib.metadata import PackageNotFoundError, version + +from packaging.version import Version + +# class VersionError(Exception): +# "Raised when there is a library version mismatch." +# def __init__(self, msg, version=None, min_version=None, **kwargs): +# self.version = version +# self.min_version = min_version +# super().__init__(msg, **kwargs) + + +def _no_import_version(name) -> tuple[int, int, int]: + """ Get version number without importing the library """ + try: + return Version(version(name)).release + except PackageNotFoundError: + return (0, 0, 0) + +# Versions +NUMPY_VERSION = _no_import_version("numpy") +PARAM_VERSION = _no_import_version("param") +PANDAS_VERSION = _no_import_version("pandas") + +NUMPY_GE_2_0_0 = NUMPY_VERSION >= (2, 0, 0) +PANDAS_GE_2_1_0 = PANDAS_VERSION >= (2, 1, 0) +PANDAS_GE_2_2_0 = PANDAS_VERSION >= (2, 2, 0) From 62053931b4fc7971399cf44c321fa2d53972fec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 13 Dec 2024 16:17:40 +0100 Subject: [PATCH 3/9] Update to gen_types --- holoviews/core/util/__init__.py | 20 +++++- holoviews/core/util/types.py | 117 ++++++++++++++++++++++++++------ holoviews/core/util/versions.py | 15 ++-- 3 files changed, 125 insertions(+), 27 deletions(-) diff --git a/holoviews/core/util/__init__.py b/holoviews/core/util/__init__.py index 72e0b833ee..99e02301a5 100644 --- a/holoviews/core/util/__init__.py +++ b/holoviews/core/util/__init__.py @@ -25,7 +25,25 @@ import pandas as pd import param -from . import versions # noqa: F401 +from .types import ( + arraylike_types, + cftime_types, + datetime_types, + generator_types, # noqa: F401 + masked_types, + pandas_datetime_types, + pandas_timedelta_types, + timedelta_types, +) +from .versions import ( # noqa: F401 + NUMPY_GE_2_0_0, + NUMPY_VERSION, + PANDAS_GE_2_1_0, + PANDAS_GE_2_2_0, + PANDAS_VERSION, + PARAM_VERSION, + VersionError, +) # Python 2 builtins basestring = str diff --git a/holoviews/core/util/types.py b/holoviews/core/util/types.py index 59339978ce..7c58277acc 100644 --- a/holoviews/core/util/types.py +++ b/holoviews/core/util/types.py @@ -1,27 +1,104 @@ +import datetime as dt +import inspect +import sys import types -from datetime import datetime as dt -import numpy as np -import pandas as pd -from pandas.core.arrays.masked import BaseMaskedArray -from pandas.core.dtypes.dtypes import DatetimeTZDtype -from pandas.core.dtypes.generic import ABCExtensionArray, ABCIndex, ABCSeries +# gen_types is copied from param, can be removed when +# we support 2.2 or greater +class _GeneratorIsMeta(type): + def __instancecheck__(cls, inst): + return isinstance(inst, tuple(cls.types())) + + def __subclasscheck__(cls, sub): + return issubclass(sub, tuple(cls.types())) + + def __iter__(cls): + yield from cls.types() + + +class _GeneratorIs(metaclass=_GeneratorIsMeta): + @classmethod + def __iter__(cls): + yield from cls.types() + + +def gen_types(gen_func): + """ + Decorator which takes a generator function which yields difference types + make it so it can be called with isinstance and issubclass.""" + if not inspect.isgeneratorfunction(gen_func): + msg = "gen_types decorator can only be applied to generator" + raise TypeError(msg) + return type(gen_func.__name__, (_GeneratorIs,), {"types": staticmethod(gen_func)}) # Types generator_types = (zip, range, types.GeneratorType) -pandas_datetime_types = (pd.Timestamp, DatetimeTZDtype, pd.Period) -pandas_timedelta_types = (pd.Timedelta,) -datetime_types = (np.datetime64, dt.datetime, dt.date, dt.time, *pandas_datetime_types) -timedelta_types = (np.timedelta64, dt.timedelta, *pandas_timedelta_types) -arraylike_types = (np.ndarray, ABCSeries, ABCIndex, ABCExtensionArray) -masked_types = (BaseMaskedArray,) - -try: - import cftime - cftime_types = (cftime.datetime,) - datetime_types += cftime_types -except ImportError: - cftime_types = () -_STANDARD_CALENDARS = {'standard', 'gregorian', 'proleptic_gregorian'} + + +@gen_types +def pandas_datetime_types(): + if pd := sys.modules.get("pandas"): + from pandas.core.dtypes.dtypes import DatetimeTZDtype + + yield from (pd.Timestamp, pd.Period, DatetimeTZDtype) + + +@gen_types +def pandas_timedelta_types(): + if pd := sys.modules.get("pandas"): + yield pd.Timedelta + + +@gen_types +def cftime_types(): + if cftime := sys.modules.get("cftime"): + yield cftime.datetime + + +@gen_types +def datetime_types(): + yield from (dt.datetime, dt.date, dt.time) + if np := sys.modules.get("numpy"): + yield np.datetime64 + yield from pandas_datetime_types() + yield from cftime_types() + + +@gen_types +def timedelta_types(): + yield dt.timedelta + if np := sys.modules.get("numpy"): + yield np.timedelta64 + yield from pandas_timedelta_types() + + +@gen_types +def arraylike_types(): + if np := sys.modules.get("numpy"): + yield np.ndarray + if "pandas" in sys.modules: + from pandas.core.dtypes.generic import ABCExtensionArray, ABCIndex, ABCSeries + + yield from (ABCIndex, ABCSeries, ABCExtensionArray) + + +@gen_types +def masked_types(): + if "pandas" in sys.modules: + from pandas.core.arrays.masked import BaseMaskedArray + + yield BaseMaskedArray + + +__all__ = [ + "arraylike_types", + "cftime_types", + "datetime_types", + "generator_types", + "masked_types", + "pandas_datetime_types", + "pandas_timedelta_types", + "timedelta_types", +] diff --git a/holoviews/core/util/versions.py b/holoviews/core/util/versions.py index b866ce5940..b2fc3d47ae 100644 --- a/holoviews/core/util/versions.py +++ b/holoviews/core/util/versions.py @@ -2,12 +2,13 @@ from packaging.version import Version -# class VersionError(Exception): -# "Raised when there is a library version mismatch." -# def __init__(self, msg, version=None, min_version=None, **kwargs): -# self.version = version -# self.min_version = min_version -# super().__init__(msg, **kwargs) + +class VersionError(Exception): + "Raised when there is a library version mismatch." + def __init__(self, msg, version=None, min_version=None, **kwargs): + self.version = version + self.min_version = min_version + super().__init__(msg, **kwargs) def _no_import_version(name) -> tuple[int, int, int]: @@ -25,3 +26,5 @@ def _no_import_version(name) -> tuple[int, int, int]: NUMPY_GE_2_0_0 = NUMPY_VERSION >= (2, 0, 0) PANDAS_GE_2_1_0 = PANDAS_VERSION >= (2, 1, 0) PANDAS_GE_2_2_0 = PANDAS_VERSION >= (2, 2, 0) + +__all__ = ["NUMPY_GE_2_0_0", "NUMPY_VERSION", "PANDAS_GE_2_1_0", "PANDAS_GE_2_2_0", "PANDAS_VERSION", "PARAM_VERSION"] From 6c599fb26c05374bb9ca55d0bca799c111aa19b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 13 Dec 2024 16:21:56 +0100 Subject: [PATCH 4/9] Update relative imports --- holoviews/core/util/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/core/util/__init__.py b/holoviews/core/util/__init__.py index 99e02301a5..ac9b7536dd 100644 --- a/holoviews/core/util/__init__.py +++ b/holoviews/core/util/__init__.py @@ -265,7 +265,7 @@ def deprecated_opts_signature(args, kwargs): Returns whether opts.apply_groups should be used (as a bool) and the corresponding options. """ - from .options import Options + from ..options import Options groups = set(Options._option_groups) opts = {kw for kw in kwargs if kw != 'clone'} apply_groups = False @@ -1675,7 +1675,7 @@ def stream_name_mapping(stream, exclude_params=None, reverse=False): """ if exclude_params is None: exclude_params = ['name'] - from ..streams import Params + from ...streams import Params if isinstance(stream, Params): mapping = {} for p in stream.parameters: @@ -1721,7 +1721,7 @@ def stream_parameters(streams, no_duplicates=True, exclude=None): """ if exclude is None: exclude = ['name', '_memoize_key'] - from ..streams import Params + from ...streams import Params param_groups = {} for s in streams: if not s.contents and isinstance(s.hashkey, dict): From 9eda759dcf2ab222f67663c96a193f6415ceb3c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 13 Dec 2024 16:22:09 +0100 Subject: [PATCH 5/9] Update isinstance --- holoviews/core/util/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/core/util/__init__.py b/holoviews/core/util/__init__.py index ac9b7536dd..e257153328 100644 --- a/holoviews/core/util/__init__.py +++ b/holoviews/core/util/__init__.py @@ -854,7 +854,7 @@ def isnat(val): return np.isnat(val) elif val is pd.NaT: return True - elif isinstance(val, pandas_datetime_types+pandas_timedelta_types): + elif isinstance(val, (pandas_datetime_types, pandas_timedelta_types)): return pd.isna(val) else: return False @@ -888,7 +888,7 @@ def isfinite(val): finite = np.isfinite(val) finite &= ~pd.isna(val) return finite - elif isinstance(val, datetime_types+timedelta_types): + elif isinstance(val, (datetime_types, timedelta_types)): return not isnat(val) elif isinstance(val, (str, bytes)): return True From f12f3dc8e622ca1b417e105f2624d96593358ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 13 Dec 2024 16:50:30 +0100 Subject: [PATCH 6/9] Update other imports --- holoviews/core/data/dask.py | 2 +- holoviews/core/data/pandas.py | 20 +++++++++++++++++--- holoviews/core/element.py | 3 ++- holoviews/core/ndmapping.py | 3 ++- holoviews/core/util/__init__.py | 13 ++++++++++++- holoviews/element/util.py | 5 ++++- holoviews/plotting/mpl/annotation.py | 2 +- 7 files changed, 39 insertions(+), 9 deletions(-) diff --git a/holoviews/core/data/dask.py b/holoviews/core/data/dask.py index 2b021f5a54..42556f5c66 100644 --- a/holoviews/core/data/dask.py +++ b/holoviews/core/data/dask.py @@ -1,7 +1,6 @@ import sys import numpy as np -import pandas as pd from .. import util from ..dimension import Dimension @@ -252,6 +251,7 @@ def aggregate(cls, dataset, dimensions, function, **kwargs): agg = getattr(reindexed, inbuilts[function.__name__])() else: raise NotImplementedError + import pandas as pd df = pd.DataFrame(agg.compute()).T dropped = [] diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index dd7bad5e85..b2f72f16a3 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -1,6 +1,6 @@ +import sys + import numpy as np -import pandas as pd -from pandas.api.types import is_numeric_dtype from .. import util from ..dimension import Dimension, dimension_name @@ -23,13 +23,26 @@ class PandasAPI: ... """ +pd = None # Set when PandasInterface.applies is successful class PandasInterface(Interface, PandasAPI): - types = (pd.DataFrame,) + types = () datatype = 'dataframe' + @classmethod + def loaded(cls): + return 'pandas' in sys.modules + + @classmethod + def applies(cls, obj): + if not cls.loaded(): + return False + global pd # noqa: PLW0603 + import pandas as pd + return isinstance(obj, (pd.DataFrame, pd.Series)) + @classmethod def dimension_type(cls, dataset, dim): return cls.dtype(dataset, dim).type @@ -289,6 +302,7 @@ def aggregate(cls, dataset, dimensions, function, **kwargs): c for c in reindexed.columns if c not in cols ] else: + from pandas.api.types import is_numeric_dtype numeric_cols = [ c for c, d in zip(reindexed.columns, reindexed.dtypes) if is_numeric_dtype(d) and c not in cols diff --git a/holoviews/core/element.py b/holoviews/core/element.py index b89176a7b1..492e37e641 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -1,7 +1,6 @@ from itertools import groupby import numpy as np -import pandas as pd import param from .dimension import Dimensioned, ViewableElement, asdim @@ -210,6 +209,8 @@ def dframe(self, dimensions=None, multi_index=False): Returns: DataFrame of columns corresponding to each dimension """ + import pandas as pd + if dimensions is None: dimensions = [d.name for d in self.dimensions()] else: diff --git a/holoviews/core/ndmapping.py b/holoviews/core/ndmapping.py index 78fb1b3aaa..7051f1768b 100644 --- a/holoviews/core/ndmapping.py +++ b/holoviews/core/ndmapping.py @@ -8,7 +8,6 @@ from operator import itemgetter import numpy as np -import pandas as pd import param from . import util @@ -891,6 +890,8 @@ def dframe(self, dimensions=None, multi_index=False): Returns: DataFrame of columns corresponding to each dimension """ + import pandas as pd + if dimensions is None: outer_dimensions = self.kdims inner_dimensions = None diff --git a/holoviews/core/util/__init__.py b/holoviews/core/util/__init__.py index e257153328..5866a7f0a0 100644 --- a/holoviews/core/util/__init__.py +++ b/holoviews/core/util/__init__.py @@ -22,7 +22,6 @@ from types import FunctionType import numpy as np -import pandas as pd import param from .types import ( @@ -185,6 +184,7 @@ def default(self, obj): obj = state.choice(obj.flat, size=_ARRAY_SAMPLE_SIZE) h.update(obj.tobytes()) return h.hexdigest() + import pandas as pd if isinstance(obj, (pd.Series, pd.DataFrame)): if len(obj) > _DATAFRAME_ROWS_LARGE: obj = obj.sample(n=_DATAFRAME_SAMPLE_SIZE, random_state=0) @@ -849,6 +849,7 @@ def isnat(val): """ Checks if the value is a NaT. Should only be called on datetimelike objects. """ + import pandas as pd if (isinstance(val, (np.datetime64, np.timedelta64)) or (isinstance(val, np.ndarray) and val.dtype.kind == 'M')): return np.isnat(val) @@ -865,6 +866,7 @@ def isfinite(val): Helper function to determine if scalar or array value is finite extending np.isfinite with support for None, string, datetime types. """ + import pandas as pd is_dask = is_dask_array(val) if not np.isscalar(val) and not is_dask: if isinstance(val, np.ma.core.MaskedArray): @@ -963,6 +965,7 @@ def max_range(ranges, combined=True): Returns: The maximum range as a single tuple """ + import pandas as pd try: with warnings.catch_warnings(): warnings.filterwarnings('ignore', r'All-NaN (slice|axis) encountered') @@ -1166,6 +1169,7 @@ def unique_array(arr): if not len(arr): return np.asarray(arr) + import pandas as pd if isinstance(arr, np.ndarray) and arr.dtype.kind not in 'MO': # Avoid expensive unpacking if not potentially datetime return pd.unique(arr) @@ -1486,6 +1490,7 @@ def is_dataframe(data): """ Checks whether the supplied data is of DataFrame type. """ + import pandas as pd dd = None if 'dask.dataframe' in sys.modules and 'pandas' in sys.modules: import dask.dataframe as dd @@ -1497,6 +1502,7 @@ def is_series(data): """ Checks whether the supplied data is of Series type. """ + import pandas as pd dd = None if 'dask.dataframe' in sys.modules: import dask.dataframe as dd @@ -1884,6 +1890,7 @@ def __call__(self, ndmapping, dimensions, container_type, @param.parameterized.bothmethod def groupby_pandas(self_or_cls, ndmapping, dimensions, container_type, group_type, sort=False, **kwargs): + import pandas as pd if 'kdims' in kwargs: idims = [ndmapping.get_dimension(d) for d in kwargs['kdims']] else: @@ -2018,6 +2025,7 @@ def is_nan(x): try: # Using pd.isna instead of np.isnan as np.isnan(pd.NA) returns pd.NA! # Call bool() to raise an error if x is pd.NA, an array, etc. + import pandas as pd return bool(pd.isna(x)) except Exception: return False @@ -2084,6 +2092,7 @@ def date_range(start, end, length, time_unit='us'): Computes a date range given a start date, end date and the number of samples. """ + import pandas as pd step = (1./compute_density(start, end, length, time_unit)) if isinstance(start, pd.Timestamp): start = start.to_datetime64() @@ -2095,6 +2104,7 @@ def parse_datetime(date): """ Parses dates specified as string or integer or pandas Timestamp """ + import pandas as pd return pd.to_datetime(date).to_datetime64() @@ -2118,6 +2128,7 @@ def dt_to_int(value, time_unit='us'): """ Converts a datetime type to an integer with the supplied time unit. """ + import pandas as pd if isinstance(value, pd.Period): value = value.to_timestamp() if isinstance(value, pd.Timestamp): diff --git a/holoviews/element/util.py b/holoviews/element/util.py index 166aeb5f3f..f84dfa7c1e 100644 --- a/holoviews/element/util.py +++ b/holoviews/element/util.py @@ -1,7 +1,6 @@ import itertools import numpy as np -import pandas as pd import param from ..core import Dataset @@ -95,6 +94,7 @@ def reduce_fn(x): """ Aggregation function to get the first non-zero value. """ + import pandas as pd values = x.values if isinstance(x, pd.Series) else x for v in values: if not is_nan(v): @@ -200,6 +200,7 @@ def _aggregate_dataset(self, obj): datatype=self.p.datatype) def _aggregate_dataset_pandas(self, obj): + import pandas as pd index_cols = [d.name for d in obj.kdims] groupby_kwargs = {"sort": False} if PANDAS_GE_2_1_0: @@ -270,6 +271,7 @@ def connect_edges_pd(graph): operation depends on pandas and is a lot faster than the pure NumPy equivalent. """ + import pandas as pd edges = graph.dframe() edges.index.name = 'graph_edge_index' edges = edges.reset_index() @@ -296,6 +298,7 @@ def connect_tri_edges_pd(trimesh): operation depends on pandas and is a lot faster than the pure NumPy equivalent. """ + import pandas as pd edges = trimesh.dframe().copy() edges.index.name = 'trimesh_edge_index' edges = edges.drop("color", errors="ignore", axis=1).reset_index() diff --git a/holoviews/plotting/mpl/annotation.py b/holoviews/plotting/mpl/annotation.py index 75783c4ac0..302e8a30c6 100644 --- a/holoviews/plotting/mpl/annotation.py +++ b/holoviews/plotting/mpl/annotation.py @@ -1,6 +1,5 @@ import matplotlib as mpl import numpy as np -import pandas as pd import param from matplotlib import patches from matplotlib.lines import Line2D @@ -306,6 +305,7 @@ def draw_annotation(self, axis, positions, opts): return [fn(*val, **opts) for val in values] def get_extents(self, element, ranges=None, range_type='combined', **kwargs): + import pandas as pd extents = super().get_extents(element, ranges, range_type) if isinstance(element, HLines): extents = np.nan, extents[0], np.nan, extents[2] From 803c1dbefe8c8d14db8b679adc57d199a1bf438c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 13 Dec 2024 16:56:49 +0100 Subject: [PATCH 7/9] Update comparison --- holoviews/element/comparison.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/holoviews/element/comparison.py b/holoviews/element/comparison.py index 2d753c6d88..90600311fd 100644 --- a/holoviews/element/comparison.py +++ b/holoviews/element/comparison.py @@ -18,12 +18,12 @@ considered different. """ import contextlib +import sys from functools import partial from unittest import TestCase from unittest.util import safe_repr import numpy as np -import pandas as pd from numpy.testing import assert_array_almost_equal, assert_array_equal from ..core import ( @@ -131,7 +131,8 @@ def register(cls): cls.equality_type_funcs[np.ma.masked_array] = cls.compare_arrays # Pandas dataframe comparison - cls.equality_type_funcs[pd.DataFrame] = cls.compare_dataframe + if pd := sys.modules.get("pandas"): + cls.equality_type_funcs[pd.DataFrame] = cls.compare_dataframe # Dimension objects cls.equality_type_funcs[Dimension] = cls.compare_dimensions From d8bb2644dca13924f8eff5e69dd78eace45ecbae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 13 Dec 2024 17:16:17 +0100 Subject: [PATCH 8/9] write todo for dimension type formatter --- holoviews/core/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/holoviews/core/__init__.py b/holoviews/core/__init__.py index f24658a029..f24db36cc3 100644 --- a/holoviews/core/__init__.py +++ b/holoviews/core/__init__.py @@ -1,7 +1,5 @@ from datetime import date, datetime -import pandas as pd - from .boundingregion import * from .data import * from .dimension import * @@ -31,7 +29,7 @@ Dimension.type_formatters[np.datetime64] = '%Y-%m-%d %H:%M:%S' Dimension.type_formatters[datetime] = '%Y-%m-%d %H:%M:%S' Dimension.type_formatters[date] = '%Y-%m-%d' -Dimension.type_formatters[pd.Timestamp] = "%Y-%m-%d %H:%M:%S" +# Dimension.type_formatters['pandas.Timestamp'] = "%Y-%m-%d %H:%M:%S" # TODO: Implement this def public(obj): From dc063df87acf87c2fd33e4f4d71b29888562ae35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 13 Dec 2024 17:16:47 +0100 Subject: [PATCH 9/9] add pandas to blocklist import --- holoviews/tests/util/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/tests/util/test_init.py b/holoviews/tests/util/test_init.py index dc5b80fc21..d16564c706 100644 --- a/holoviews/tests/util/test_init.py +++ b/holoviews/tests/util/test_init.py @@ -8,7 +8,7 @@ def test_no_blocklist_imports(): import sys import holoviews as hv - blocklist = {"panel", "IPython", "datashader", "ibis"} + blocklist = {"panel", "IPython", "datashader", "ibis", "pandas"} mods = blocklist & set(sys.modules) if mods: