diff --git a/asv_bench/benchmarks/reshape.py b/asv_bench/benchmarks/reshape.py index 7046c8862b0d7..b42729476c818 100644 --- a/asv_bench/benchmarks/reshape.py +++ b/asv_bench/benchmarks/reshape.py @@ -268,7 +268,9 @@ def setup(self, bins): self.datetime_series = pd.Series( np.random.randint(N, size=N), dtype="datetime64[ns]" ) - self.interval_bins = pd.IntervalIndex.from_breaks(np.linspace(0, N, bins)) + self.interval_bins = pd.IntervalIndex.from_breaks( + np.linspace(0, N, bins), "right" + ) def time_cut_int(self, bins): pd.cut(self.int_series, bins) diff --git a/doc/redirects.csv b/doc/redirects.csv index 9b8a5a73dedff..173e670e30f0e 100644 --- a/doc/redirects.csv +++ b/doc/redirects.csv @@ -741,11 +741,11 @@ generated/pandas.Index.values,../reference/api/pandas.Index.values generated/pandas.Index.view,../reference/api/pandas.Index.view generated/pandas.Index.where,../reference/api/pandas.Index.where generated/pandas.infer_freq,../reference/api/pandas.infer_freq -generated/pandas.Interval.closed,../reference/api/pandas.Interval.closed +generated/pandas.Interval.inclusive,../reference/api/pandas.Interval.inclusive generated/pandas.Interval.closed_left,../reference/api/pandas.Interval.closed_left generated/pandas.Interval.closed_right,../reference/api/pandas.Interval.closed_right generated/pandas.Interval,../reference/api/pandas.Interval -generated/pandas.IntervalIndex.closed,../reference/api/pandas.IntervalIndex.closed +generated/pandas.IntervalIndex.inclusive,../reference/api/pandas.IntervalIndex.inclusive generated/pandas.IntervalIndex.contains,../reference/api/pandas.IntervalIndex.contains generated/pandas.IntervalIndex.from_arrays,../reference/api/pandas.IntervalIndex.from_arrays generated/pandas.IntervalIndex.from_breaks,../reference/api/pandas.IntervalIndex.from_breaks diff --git a/doc/source/reference/arrays.rst b/doc/source/reference/arrays.rst index 1b8e0fdb856b5..fed0d2c5f7827 100644 --- a/doc/source/reference/arrays.rst +++ b/doc/source/reference/arrays.rst @@ -303,7 +303,7 @@ Properties .. autosummary:: :toctree: api/ - Interval.closed + Interval.inclusive Interval.closed_left Interval.closed_right Interval.is_empty @@ -340,7 +340,7 @@ A collection of intervals may be stored in an :class:`arrays.IntervalArray`. arrays.IntervalArray.left arrays.IntervalArray.right - arrays.IntervalArray.closed + arrays.IntervalArray.inclusive arrays.IntervalArray.mid arrays.IntervalArray.length arrays.IntervalArray.is_empty diff --git a/doc/source/reference/indexing.rst b/doc/source/reference/indexing.rst index ddfef14036ef3..89a9a0a92ef08 100644 --- a/doc/source/reference/indexing.rst +++ b/doc/source/reference/indexing.rst @@ -242,7 +242,7 @@ IntervalIndex components IntervalIndex.left IntervalIndex.right IntervalIndex.mid - IntervalIndex.closed + IntervalIndex.inclusive IntervalIndex.length IntervalIndex.values IntervalIndex.is_empty diff --git a/doc/source/user_guide/advanced.rst b/doc/source/user_guide/advanced.rst index 3081c6f7c6a08..aaff76261b3ad 100644 --- a/doc/source/user_guide/advanced.rst +++ b/doc/source/user_guide/advanced.rst @@ -1020,7 +1020,7 @@ Trying to select an ``Interval`` that is not exactly contained in the ``Interval In [7]: df.loc[pd.Interval(0.5, 2.5)] --------------------------------------------------------------------------- - KeyError: Interval(0.5, 2.5, closed='right') + KeyError: Interval(0.5, 2.5, inclusive='right') Selecting all ``Intervals`` that overlap a given ``Interval`` can be performed using the :meth:`~IntervalIndex.overlaps` method to create a boolean indexer. diff --git a/doc/source/whatsnew/v0.20.0.rst b/doc/source/whatsnew/v0.20.0.rst index faf4b1ac44d5b..a23c977e94b65 100644 --- a/doc/source/whatsnew/v0.20.0.rst +++ b/doc/source/whatsnew/v0.20.0.rst @@ -448,7 +448,7 @@ Selecting via a specific interval: .. ipython:: python - df.loc[pd.Interval(1.5, 3.0)] + df.loc[pd.Interval(1.5, 3.0, "right")] Selecting via a scalar value that is contained *in* the intervals. diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index e4dd6fa091d80..4f04d5a0ee69d 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -584,18 +584,18 @@ this would previously return ``True`` for any ``Interval`` overlapping an ``Inte .. code-block:: python - In [4]: pd.Interval(1, 2, closed='neither') in ii + In [4]: pd.Interval(1, 2, inclusive='neither') in ii Out[4]: True - In [5]: pd.Interval(-10, 10, closed='both') in ii + In [5]: pd.Interval(-10, 10, inclusive='both') in ii Out[5]: True *New behavior*: .. ipython:: python - pd.Interval(1, 2, closed='neither') in ii - pd.Interval(-10, 10, closed='both') in ii + pd.Interval(1, 2, inclusive='neither') in ii + pd.Interval(-10, 10, inclusive='both') in ii The :meth:`~IntervalIndex.get_loc` method now only returns locations for exact matches to ``Interval`` queries, as opposed to the previous behavior of returning locations for overlapping matches. A ``KeyError`` will be raised if an exact match is not found. @@ -619,7 +619,7 @@ returning locations for overlapping matches. A ``KeyError`` will be raised if a In [7]: ii.get_loc(pd.Interval(2, 6)) --------------------------------------------------------------------------- - KeyError: Interval(2, 6, closed='right') + KeyError: Interval(2, 6, inclusive='right') Likewise, :meth:`~IntervalIndex.get_indexer` and :meth:`~IntervalIndex.get_indexer_non_unique` will also only return locations for exact matches to ``Interval`` queries, with ``-1`` denoting that an exact match was not found. @@ -680,11 +680,11 @@ Similarly, a ``KeyError`` will be raised for non-exact matches instead of return In [6]: s[pd.Interval(2, 3)] --------------------------------------------------------------------------- - KeyError: Interval(2, 3, closed='right') + KeyError: Interval(2, 3, inclusive='right') In [7]: s.loc[pd.Interval(2, 3)] --------------------------------------------------------------------------- - KeyError: Interval(2, 3, closed='right') + KeyError: Interval(2, 3, inclusive='right') The :meth:`~IntervalIndex.overlaps` method can be used to create a boolean indexer that replicates the previous behavior of returning overlapping matches. diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index eb08034bb92eb..ac9f8b02c7acb 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -669,7 +669,12 @@ Other Deprecations - Deprecated the methods :meth:`DataFrame.mad`, :meth:`Series.mad`, and the corresponding groupby methods (:issue:`11787`) - Deprecated positional arguments to :meth:`Index.join` except for ``other``, use keyword-only arguments instead of positional arguments (:issue:`46518`) - Deprecated indexing on a timezone-naive :class:`DatetimeIndex` using a string representing a timezone-aware datetime (:issue:`46903`, :issue:`36148`) -- +- Deprecated the ``closed`` argument in :class:`Interval` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) +- Deprecated the ``closed`` argument in :class:`IntervalIndex` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) +- Deprecated the ``closed`` argument in :class:`IntervalDtype` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) +- Deprecated the ``closed`` argument in :class:`IntervalArray` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) +- Deprecated the ``closed`` argument in :class:`intervaltree` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) +- Deprecated the ``closed`` argument in :class:`ArrowInterval` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) .. --------------------------------------------------------------------------- .. _whatsnew_150.performance: diff --git a/pandas/_libs/interval.pyi b/pandas/_libs/interval.pyi index 9b727b6278792..d177e597478d9 100644 --- a/pandas/_libs/interval.pyi +++ b/pandas/_libs/interval.pyi @@ -11,6 +11,7 @@ from typing import ( import numpy as np import numpy.typing as npt +from pandas._libs import lib from pandas._typing import ( IntervalClosedType, Timedelta, @@ -54,19 +55,24 @@ class IntervalMixin: def is_empty(self) -> bool: ... def _check_closed_matches(self, other: IntervalMixin, name: str = ...) -> None: ... +def _warning_interval( + inclusive, closed +) -> tuple[IntervalClosedType, lib.NoDefault]: ... + class Interval(IntervalMixin, Generic[_OrderableT]): @property def left(self: Interval[_OrderableT]) -> _OrderableT: ... @property def right(self: Interval[_OrderableT]) -> _OrderableT: ... @property - def closed(self) -> IntervalClosedType: ... + def inclusive(self) -> IntervalClosedType: ... mid: _MidDescriptor length: _LengthDescriptor def __init__( self, left: _OrderableT, right: _OrderableT, + inclusive: IntervalClosedType = ..., closed: IntervalClosedType = ..., ) -> None: ... def __hash__(self) -> int: ... @@ -157,7 +163,7 @@ class IntervalTree(IntervalMixin): self, left: np.ndarray, right: np.ndarray, - closed: IntervalClosedType = ..., + inclusive: IntervalClosedType = ..., leaf_size: int = ..., ) -> None: ... @property diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 44c50e64147f4..178836ff1548b 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -40,7 +40,9 @@ from numpy cimport ( cnp.import_array() +import warnings +from pandas._libs import lib from pandas._libs cimport util from pandas._libs.hashtable cimport Int64Vector from pandas._libs.tslibs.timedeltas cimport _Timedelta @@ -52,7 +54,7 @@ from pandas._libs.tslibs.util cimport ( is_timedelta64_object, ) -VALID_CLOSED = frozenset(['left', 'right', 'both', 'neither']) +VALID_CLOSED = frozenset(['both', 'neither', 'left', 'right']) cdef class IntervalMixin: @@ -69,7 +71,7 @@ cdef class IntervalMixin: bool True if the Interval is closed on the left-side. """ - return self.closed in ('left', 'both') + return self.inclusive in ('left', 'both') @property def closed_right(self): @@ -83,7 +85,7 @@ cdef class IntervalMixin: bool True if the Interval is closed on the left-side. """ - return self.closed in ('right', 'both') + return self.inclusive in ('right', 'both') @property def open_left(self): @@ -150,43 +152,43 @@ cdef class IntervalMixin: -------- An :class:`Interval` that contains points is not empty: - >>> pd.Interval(0, 1, closed='right').is_empty + >>> pd.Interval(0, 1, inclusive='right').is_empty False An ``Interval`` that does not contain any points is empty: - >>> pd.Interval(0, 0, closed='right').is_empty + >>> pd.Interval(0, 0, inclusive='right').is_empty True - >>> pd.Interval(0, 0, closed='left').is_empty + >>> pd.Interval(0, 0, inclusive='left').is_empty True - >>> pd.Interval(0, 0, closed='neither').is_empty + >>> pd.Interval(0, 0, inclusive='neither').is_empty True An ``Interval`` that contains a single point is not empty: - >>> pd.Interval(0, 0, closed='both').is_empty + >>> pd.Interval(0, 0, inclusive='both').is_empty False An :class:`~arrays.IntervalArray` or :class:`IntervalIndex` returns a boolean ``ndarray`` positionally indicating if an ``Interval`` is empty: - >>> ivs = [pd.Interval(0, 0, closed='neither'), - ... pd.Interval(1, 2, closed='neither')] + >>> ivs = [pd.Interval(0, 0, inclusive='neither'), + ... pd.Interval(1, 2, inclusive='neither')] >>> pd.arrays.IntervalArray(ivs).is_empty array([ True, False]) Missing values are not considered empty: - >>> ivs = [pd.Interval(0, 0, closed='neither'), np.nan] + >>> ivs = [pd.Interval(0, 0, inclusive='neither'), np.nan] >>> pd.IntervalIndex(ivs).is_empty array([ True, False]) """ - return (self.right == self.left) & (self.closed != 'both') + return (self.right == self.left) & (self.inclusive != 'both') def _check_closed_matches(self, other, name='other'): """ - Check if the closed attribute of `other` matches. + Check if the inclusive attribute of `other` matches. Note that 'left' and 'right' are considered different from 'both'. @@ -201,16 +203,42 @@ cdef class IntervalMixin: ValueError When `other` is not closed exactly the same as self. """ - if self.closed != other.closed: - raise ValueError(f"'{name}.closed' is {repr(other.closed)}, " - f"expected {repr(self.closed)}.") + if self.inclusive != other.inclusive: + raise ValueError(f"'{name}.inclusive' is {repr(other.inclusive)}, " + f"expected {repr(self.inclusive)}.") cdef bint _interval_like(other): return (hasattr(other, 'left') and hasattr(other, 'right') - and hasattr(other, 'closed')) + and hasattr(other, 'inclusive')) +def _warning_interval(inclusive: str | None = None, closed: None | lib.NoDefault = lib.no_default): + """ + warning in interval class for variable inclusive and closed + """ + if inclusive is not None and closed != lib.no_default: + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif closed != lib.no_default: + warnings.warn( + "Argument `closed` is deprecated in favor of `inclusive`.", + FutureWarning, + stacklevel=2, + ) + if closed is None: + inclusive = "both" + elif closed in ("both", "neither", "left", "right"): + inclusive = closed + else: + raise ValueError( + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" + ) + + return inclusive, closed cdef class Interval(IntervalMixin): """ @@ -226,6 +254,14 @@ cdef class Interval(IntervalMixin): Whether the interval is closed on the left-side, right-side, both or neither. See the Notes for more detailed explanation. + .. deprecated:: 1.5.0 + + inclusive : {'both', 'neither', 'left', 'right'}, default 'both' + Whether the interval is closed on the left-side, right-side, both or + neither. See the Notes for more detailed explanation. + + .. versionadded:: 1.5.0 + See Also -------- IntervalIndex : An Index of Interval objects that are all closed on the @@ -243,28 +279,28 @@ cdef class Interval(IntervalMixin): A closed interval (in mathematics denoted by square brackets) contains its endpoints, i.e. the closed interval ``[0, 5]`` is characterized by the - conditions ``0 <= x <= 5``. This is what ``closed='both'`` stands for. + conditions ``0 <= x <= 5``. This is what ``inclusive='both'`` stands for. An open interval (in mathematics denoted by parentheses) does not contain its endpoints, i.e. the open interval ``(0, 5)`` is characterized by the - conditions ``0 < x < 5``. This is what ``closed='neither'`` stands for. + conditions ``0 < x < 5``. This is what ``inclusive='neither'`` stands for. Intervals can also be half-open or half-closed, i.e. ``[0, 5)`` is - described by ``0 <= x < 5`` (``closed='left'``) and ``(0, 5]`` is - described by ``0 < x <= 5`` (``closed='right'``). + described by ``0 <= x < 5`` (``inclusive='left'``) and ``(0, 5]`` is + described by ``0 < x <= 5`` (``inclusive='right'``). Examples -------- It is possible to build Intervals of different types, like numeric ones: - >>> iv = pd.Interval(left=0, right=5) + >>> iv = pd.Interval(left=0, right=5, inclusive='right') >>> iv - Interval(0, 5, closed='right') + Interval(0, 5, inclusive='right') You can check if an element belongs to it >>> 2.5 in iv True - You can test the bounds (``closed='right'``, so ``0 < x <= 5``): + You can test the bounds (``inclusive='right'``, so ``0 < x <= 5``): >>> 0 in iv False @@ -284,16 +320,16 @@ cdef class Interval(IntervalMixin): >>> shifted_iv = iv + 3 >>> shifted_iv - Interval(3, 8, closed='right') + Interval(3, 8, inclusive='right') >>> extended_iv = iv * 10.0 >>> extended_iv - Interval(0.0, 50.0, closed='right') + Interval(0.0, 50.0, inclusive='right') To create a time interval you can use Timestamps as the bounds >>> year_2017 = pd.Interval(pd.Timestamp('2017-01-01 00:00:00'), ... pd.Timestamp('2018-01-01 00:00:00'), - ... closed='left') + ... inclusive='left') >>> pd.Timestamp('2017-01-01 00:00') in year_2017 True >>> year_2017.length @@ -312,21 +348,26 @@ cdef class Interval(IntervalMixin): Right bound for the interval. """ - cdef readonly str closed + cdef readonly str inclusive """ Whether the interval is closed on the left-side, right-side, both or neither. """ - def __init__(self, left, right, str closed='right'): + def __init__(self, left, right, inclusive: str | None = None, closed: None | lib.NoDefault = lib.no_default): # note: it is faster to just do these checks than to use a special # constructor (__cinit__/__new__) to avoid them self._validate_endpoint(left) self._validate_endpoint(right) - if closed not in VALID_CLOSED: - raise ValueError(f"invalid option for 'closed': {closed}") + inclusive, closed = _warning_interval(inclusive, closed) + + if inclusive is None: + inclusive = "both" + + if inclusive not in VALID_CLOSED: + raise ValueError(f"invalid option for 'inclusive': {inclusive}") if not left <= right: raise ValueError("left side of interval must be <= right side") if (isinstance(left, _Timestamp) and @@ -336,7 +377,7 @@ cdef class Interval(IntervalMixin): f"{repr(left.tzinfo)}' and {repr(right.tzinfo)}") self.left = left self.right = right - self.closed = closed + self.inclusive = inclusive def _validate_endpoint(self, endpoint): # GH 23013 @@ -346,7 +387,7 @@ cdef class Interval(IntervalMixin): "are allowed when constructing an Interval.") def __hash__(self): - return hash((self.left, self.right, self.closed)) + return hash((self.left, self.right, self.inclusive)) def __contains__(self, key) -> bool: if _interval_like(key): @@ -356,8 +397,8 @@ cdef class Interval(IntervalMixin): def __richcmp__(self, other, op: int): if isinstance(other, Interval): - self_tuple = (self.left, self.right, self.closed) - other_tuple = (other.left, other.right, other.closed) + self_tuple = (self.left, self.right, self.inclusive) + other_tuple = (other.left, other.right, other.inclusive) return PyObject_RichCompare(self_tuple, other_tuple, op) elif util.is_array(other): return np.array( @@ -368,7 +409,7 @@ cdef class Interval(IntervalMixin): return NotImplemented def __reduce__(self): - args = (self.left, self.right, self.closed) + args = (self.left, self.right, self.inclusive) return (type(self), args) def _repr_base(self): @@ -386,7 +427,7 @@ cdef class Interval(IntervalMixin): left, right = self._repr_base() name = type(self).__name__ - repr_str = f'{name}({repr(left)}, {repr(right)}, closed={repr(self.closed)})' + repr_str = f'{name}({repr(left)}, {repr(right)}, inclusive={repr(self.inclusive)})' return repr_str def __str__(self) -> str: @@ -402,7 +443,7 @@ cdef class Interval(IntervalMixin): or PyDelta_Check(y) or is_timedelta64_object(y) ): - return Interval(self.left + y, self.right + y, closed=self.closed) + return Interval(self.left + y, self.right + y, inclusive=self.inclusive) elif ( # __radd__ pattern # TODO(cython3): remove this @@ -413,7 +454,7 @@ cdef class Interval(IntervalMixin): or is_timedelta64_object(self) ) ): - return Interval(y.left + self, y.right + self, closed=y.closed) + return Interval(y.left + self, y.right + self, inclusive=y.inclusive) return NotImplemented def __radd__(self, other): @@ -422,7 +463,7 @@ cdef class Interval(IntervalMixin): or PyDelta_Check(other) or is_timedelta64_object(other) ): - return Interval(self.left + other, self.right + other, closed=self.closed) + return Interval(self.left + other, self.right + other, inclusive=self.inclusive) return NotImplemented def __sub__(self, y): @@ -431,32 +472,33 @@ cdef class Interval(IntervalMixin): or PyDelta_Check(y) or is_timedelta64_object(y) ): - return Interval(self.left - y, self.right - y, closed=self.closed) + return Interval(self.left - y, self.right - y, inclusive=self.inclusive) return NotImplemented def __mul__(self, y): if isinstance(y, numbers.Number): - return Interval(self.left * y, self.right * y, closed=self.closed) + return Interval(self.left * y, self.right * y, inclusive=self.inclusive) elif isinstance(y, Interval) and isinstance(self, numbers.Number): # __radd__ semantics # TODO(cython3): remove this - return Interval(y.left * self, y.right * self, closed=y.closed) + return Interval(y.left * self, y.right * self, inclusive=y.inclusive) + return NotImplemented def __rmul__(self, other): if isinstance(other, numbers.Number): - return Interval(self.left * other, self.right * other, closed=self.closed) + return Interval(self.left * other, self.right * other, inclusive=self.inclusive) return NotImplemented def __truediv__(self, y): if isinstance(y, numbers.Number): - return Interval(self.left / y, self.right / y, closed=self.closed) + return Interval(self.left / y, self.right / y, inclusive=self.inclusive) return NotImplemented def __floordiv__(self, y): if isinstance(y, numbers.Number): return Interval( - self.left // y, self.right // y, closed=self.closed) + self.left // y, self.right // y, inclusive=self.inclusive) return NotImplemented def overlaps(self, other): @@ -494,14 +536,14 @@ cdef class Interval(IntervalMixin): Intervals that share closed endpoints overlap: - >>> i4 = pd.Interval(0, 1, closed='both') - >>> i5 = pd.Interval(1, 2, closed='both') + >>> i4 = pd.Interval(0, 1, inclusive='both') + >>> i5 = pd.Interval(1, 2, inclusive='both') >>> i4.overlaps(i5) True Intervals that only have an open endpoint in common do not overlap: - >>> i6 = pd.Interval(1, 2, closed='neither') + >>> i6 = pd.Interval(1, 2, inclusive='neither') >>> i4.overlaps(i6) False """ @@ -537,10 +579,10 @@ def intervals_to_interval_bounds(ndarray intervals, bint validate_closed=True): tuple of left : ndarray right : ndarray - closed: str + inclusive: str """ cdef: - object closed = None, interval + object inclusive = None, interval Py_ssize_t i, n = len(intervals) ndarray left, right bint seen_closed = False @@ -563,13 +605,13 @@ def intervals_to_interval_bounds(ndarray intervals, bint validate_closed=True): right[i] = interval.right if not seen_closed: seen_closed = True - closed = interval.closed - elif closed != interval.closed: - closed = None + inclusive = interval.inclusive + elif inclusive != interval.inclusive: + inclusive = None if validate_closed: raise ValueError("intervals must all be closed on the same side") - return left, right, closed + return left, right, inclusive include "intervaltree.pxi" diff --git a/pandas/_libs/intervaltree.pxi.in b/pandas/_libs/intervaltree.pxi.in index 547fcc0b8aa07..51db5f1e76c99 100644 --- a/pandas/_libs/intervaltree.pxi.in +++ b/pandas/_libs/intervaltree.pxi.in @@ -3,9 +3,13 @@ Template for intervaltree WARNING: DO NOT edit .pxi FILE directly, .pxi is generated from .pxi.in """ +import warnings +from pandas._libs import lib from pandas._libs.algos import is_monotonic +from pandas._libs.interval import _warning_interval + ctypedef fused int_scalar_t: int64_t float64_t @@ -34,11 +38,11 @@ cdef class IntervalTree(IntervalMixin): ndarray left, right IntervalNode root object dtype - str closed + str inclusive object _is_overlapping, _left_sorter, _right_sorter Py_ssize_t _na_count - def __init__(self, left, right, closed='right', leaf_size=100): + def __init__(self, left, right, inclusive: str | None = None, closed: None | lib.NoDefault = lib.no_default, leaf_size=100): """ Parameters ---------- @@ -48,13 +52,27 @@ cdef class IntervalTree(IntervalMixin): closed : {'left', 'right', 'both', 'neither'}, optional Whether the intervals are closed on the left-side, right-side, both or neither. Defaults to 'right'. + + .. deprecated:: 1.5.0 + + inclusive : {"both", "neither", "left", "right"}, optional + Whether the intervals are closed on the left-side, right-side, both + or neither. Defaults to 'right'. + + .. versionadded:: 1.5.0 + leaf_size : int, optional Parameter that controls when the tree switches from creating nodes to brute-force search. Tune this parameter to optimize query performance. """ - if closed not in ['left', 'right', 'both', 'neither']: - raise ValueError("invalid option for 'closed': %s" % closed) + inclusive, closed = _warning_interval(inclusive, closed) + + if inclusive is None: + inclusive = "both" + + if inclusive not in ['left', 'right', 'both', 'neither']: + raise ValueError("invalid option for 'inclusive': %s" % inclusive) left = np.asarray(left) right = np.asarray(right) @@ -64,7 +82,7 @@ cdef class IntervalTree(IntervalMixin): indices = np.arange(len(left), dtype='int64') - self.closed = closed + self.inclusive = inclusive # GH 23352: ensure no nan in nodes mask = ~np.isnan(self.left) @@ -73,7 +91,7 @@ cdef class IntervalTree(IntervalMixin): self.right = self.right[mask] indices = indices[mask] - node_cls = NODE_CLASSES[str(self.dtype), closed] + node_cls = NODE_CLASSES[str(self.dtype), inclusive] self.root = node_cls(self.left, self.right, indices, leaf_size) @property @@ -102,7 +120,7 @@ cdef class IntervalTree(IntervalMixin): return self._is_overlapping # <= when both sides closed since endpoints can overlap - op = le if self.closed == 'both' else lt + op = le if self.inclusive == 'both' else lt # overlap if start of current interval < end of previous interval # (current and previous in terms of sorted order by left/start side) @@ -180,9 +198,9 @@ cdef class IntervalTree(IntervalMixin): missing.to_array().astype('intp')) def __repr__(self) -> str: - return (''.format( - dtype=self.dtype, closed=self.closed, + dtype=self.dtype, inclusive=self.inclusive, n_elements=self.root.n_elements)) # compat with IndexEngine interface @@ -251,7 +269,7 @@ cdef class IntervalNode: nodes = [] for dtype in ['float64', 'int64', 'uint64']: - for closed, cmp_left, cmp_right in [ + for inclusive, cmp_left, cmp_right in [ ('left', '<=', '<'), ('right', '<', '<='), ('both', '<=', '<='), @@ -265,7 +283,7 @@ for dtype in ['float64', 'int64', 'uint64']: elif dtype.startswith('float'): fused_prefix = '' nodes.append((dtype, dtype.title(), - closed, closed.title(), + inclusive, inclusive.title(), cmp_left, cmp_right, cmp_left_converse, diff --git a/pandas/_libs/lib.pyx b/pandas/_libs/lib.pyx index 4e245d1bd8693..2136c410ef4a0 100644 --- a/pandas/_libs/lib.pyx +++ b/pandas/_libs/lib.pyx @@ -2142,7 +2142,7 @@ cpdef bint is_interval_array(ndarray values): """ cdef: Py_ssize_t i, n = len(values) - str closed = None + str inclusive = None bint numeric = False bint dt64 = False bint td64 = False @@ -2155,15 +2155,15 @@ cpdef bint is_interval_array(ndarray values): val = values[i] if is_interval(val): - if closed is None: - closed = val.closed + if inclusive is None: + inclusive = val.inclusive numeric = ( util.is_float_object(val.left) or util.is_integer_object(val.left) ) td64 = is_timedelta(val.left) dt64 = PyDateTime_Check(val.left) - elif val.closed != closed: + elif val.inclusive != inclusive: # mismatched closedness return False elif numeric: @@ -2186,7 +2186,7 @@ cpdef bint is_interval_array(ndarray values): else: return False - if closed is None: + if inclusive is None: # we saw all-NAs, no actual Intervals return False return True diff --git a/pandas/_testing/asserters.py b/pandas/_testing/asserters.py index efbe9995525d7..d04dc76c01f7e 100644 --- a/pandas/_testing/asserters.py +++ b/pandas/_testing/asserters.py @@ -606,7 +606,7 @@ def assert_interval_array_equal(left, right, exact="equiv", obj="IntervalArray") assert_equal(left._left, right._left, obj=f"{obj}.left", **kwargs) assert_equal(left._right, right._right, obj=f"{obj}.left", **kwargs) - assert_attr_equal("closed", left, right, obj=obj) + assert_attr_equal("inclusive", left, right, obj=obj) def assert_period_array_equal(left, right, obj="PeriodArray"): diff --git a/pandas/conftest.py b/pandas/conftest.py index 73ced327ac4a9..dfe8c5f1778d3 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -606,7 +606,7 @@ def _create_mi_with_dt64tz_level(): "bool-object": tm.makeBoolIndex(10).astype(object), "bool-dtype": Index(np.random.randn(10) < 0), "categorical": tm.makeCategoricalIndex(100), - "interval": tm.makeIntervalIndex(100), + "interval": tm.makeIntervalIndex(100, inclusive="right"), "empty": Index([]), "tuples": MultiIndex.from_tuples(zip(["foo", "bar", "baz"], [1, 2, 3])), "mi-with-dt64tz-level": _create_mi_with_dt64tz_level(), @@ -934,8 +934,14 @@ def rand_series_with_duplicate_datetimeindex(): # ---------------------------------------------------------------- @pytest.fixture( params=[ - (Interval(left=0, right=5), IntervalDtype("int64", "right")), - (Interval(left=0.1, right=0.5), IntervalDtype("float64", "right")), + ( + Interval(left=0, right=5, inclusive="right"), + IntervalDtype("int64", inclusive="right"), + ), + ( + Interval(left=0.1, right=0.5, inclusive="right"), + IntervalDtype("float64", inclusive="right"), + ), (Period("2012-01", freq="M"), "period[M]"), (Period("2012-02-01", freq="D"), "period[D]"), ( diff --git a/pandas/core/arrays/arrow/_arrow_utils.py b/pandas/core/arrays/arrow/_arrow_utils.py index ca7ec0ef2ebaf..e0f242e2ced5d 100644 --- a/pandas/core/arrays/arrow/_arrow_utils.py +++ b/pandas/core/arrays/arrow/_arrow_utils.py @@ -6,6 +6,8 @@ import numpy as np import pyarrow +from pandas._libs import lib +from pandas._libs.interval import _warning_interval from pandas.errors import PerformanceWarning from pandas.util._exceptions import find_stack_level @@ -103,11 +105,17 @@ def to_pandas_dtype(self): class ArrowIntervalType(pyarrow.ExtensionType): - def __init__(self, subtype, closed) -> None: + def __init__( + self, + subtype, + inclusive: str | None = None, + closed: None | lib.NoDefault = lib.no_default, + ) -> None: # attributes need to be set first before calling # super init (as that calls serialize) - assert closed in VALID_CLOSED - self._closed = closed + inclusive, closed = _warning_interval(inclusive, closed) + assert inclusive in VALID_CLOSED + self._closed = inclusive if not isinstance(subtype, pyarrow.DataType): subtype = pyarrow.type_for_alias(str(subtype)) self._subtype = subtype @@ -120,37 +128,37 @@ def subtype(self): return self._subtype @property - def closed(self): + def inclusive(self): return self._closed def __arrow_ext_serialize__(self): - metadata = {"subtype": str(self.subtype), "closed": self.closed} + metadata = {"subtype": str(self.subtype), "inclusive": self.inclusive} return json.dumps(metadata).encode() @classmethod def __arrow_ext_deserialize__(cls, storage_type, serialized): metadata = json.loads(serialized.decode()) subtype = pyarrow.type_for_alias(metadata["subtype"]) - closed = metadata["closed"] - return ArrowIntervalType(subtype, closed) + inclusive = metadata["inclusive"] + return ArrowIntervalType(subtype, inclusive) def __eq__(self, other): if isinstance(other, pyarrow.BaseExtensionType): return ( type(self) == type(other) and self.subtype == other.subtype - and self.closed == other.closed + and self.inclusive == other.inclusive ) else: return NotImplemented def __hash__(self): - return hash((str(self), str(self.subtype), self.closed)) + return hash((str(self), str(self.subtype), self.inclusive)) def to_pandas_dtype(self): import pandas as pd - return pd.IntervalDtype(self.subtype.to_pandas_dtype(), self.closed) + return pd.IntervalDtype(self.subtype.to_pandas_dtype(), self.inclusive) # register the type with a dummy instance diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 4c81fe8b61a1f..eecf1dff4dd48 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -24,6 +24,7 @@ VALID_CLOSED, Interval, IntervalMixin, + _warning_interval, intervals_to_interval_bounds, ) from pandas._libs.missing import NA @@ -122,7 +123,7 @@ data : array-like (1-dimensional) Array-like containing Interval objects from which to build the %(klass)s. -closed : {'left', 'right', 'both', 'neither'}, default 'right' +inclusive : {'left', 'right', 'both', 'neither'}, default 'right' Whether the intervals are closed on the left-side, right-side, both or neither. dtype : dtype or None, default None @@ -137,7 +138,7 @@ ---------- left right -closed +inclusive mid length is_empty @@ -189,7 +190,8 @@ A new ``IntervalArray`` can be constructed directly from an array-like of ``Interval`` objects: - >>> pd.arrays.IntervalArray([pd.Interval(0, 1), pd.Interval(1, 5)]) + >>> pd.arrays.IntervalArray([pd.Interval(0, 1, "right"), + ... pd.Interval(1, 5, "right")]) [(0, 1], (1, 5]] Length: 2, dtype: interval[int64, right] @@ -217,18 +219,20 @@ class IntervalArray(IntervalMixin, ExtensionArray): def __new__( cls: type[IntervalArrayT], data, - closed=None, + inclusive: str | None = None, + closed: None | lib.NoDefault = lib.no_default, dtype: Dtype | None = None, copy: bool = False, verify_integrity: bool = True, ): + inclusive, closed = _warning_interval(inclusive, closed) data = extract_array(data, extract_numpy=True) if isinstance(data, cls): left = data._left right = data._right - closed = closed or data.closed + inclusive = inclusive or data.inclusive else: # don't allow scalars @@ -242,17 +246,17 @@ def __new__( # might need to convert empty or purely na data data = _maybe_convert_platform_interval(data) left, right, infer_closed = intervals_to_interval_bounds( - data, validate_closed=closed is None + data, validate_closed=inclusive is None ) if left.dtype == object: left = lib.maybe_convert_objects(left) right = lib.maybe_convert_objects(right) - closed = closed or infer_closed + inclusive = inclusive or infer_closed return cls._simple_new( left, right, - closed, + inclusive=inclusive, copy=copy, dtype=dtype, verify_integrity=verify_integrity, @@ -263,17 +267,21 @@ def _simple_new( cls: type[IntervalArrayT], left, right, - closed: IntervalClosedType | None = None, + inclusive=None, + closed: None | lib.NoDefault = lib.no_default, copy: bool = False, dtype: Dtype | None = None, verify_integrity: bool = True, ) -> IntervalArrayT: result = IntervalMixin.__new__(cls) - if closed is None and isinstance(dtype, IntervalDtype): - closed = dtype.closed + inclusive, closed = _warning_interval(inclusive, closed) + + if inclusive is None and isinstance(dtype, IntervalDtype): + inclusive = dtype.inclusive + + inclusive = inclusive or "both" - closed = closed or "right" left = ensure_index(left, copy=copy) right = ensure_index(right, copy=copy) @@ -288,12 +296,11 @@ def _simple_new( else: msg = f"dtype must be an IntervalDtype, got {dtype}" raise TypeError(msg) - - if dtype.closed is None: + if dtype.inclusive is None: # possibly loading an old pickle - dtype = IntervalDtype(dtype.subtype, closed) - elif closed != dtype.closed: - raise ValueError("closed keyword does not match dtype.closed") + dtype = IntervalDtype(dtype.subtype, inclusive) + elif inclusive != dtype.inclusive: + raise ValueError("inclusive keyword does not match dtype.inclusive") # coerce dtypes to match if needed if is_float_dtype(left) and is_integer_dtype(right): @@ -336,7 +343,7 @@ def _simple_new( # If these share data, then setitem could corrupt our IA right = right.copy() - dtype = IntervalDtype(left.dtype, closed=closed) + dtype = IntervalDtype(left.dtype, inclusive=inclusive) result._dtype = dtype result._left = left @@ -364,7 +371,7 @@ def _from_factorized( # a new IA from an (empty) object-dtype array, so turn it into the # correct dtype. values = values.astype(original.dtype.subtype) - return cls(values, closed=original.closed) + return cls(values, inclusive=original.inclusive) _interval_shared_docs["from_breaks"] = textwrap.dedent( """ @@ -374,7 +381,7 @@ def _from_factorized( ---------- breaks : array-like (1-dimensional) Left and right bounds for each interval. - closed : {'left', 'right', 'both', 'neither'}, default 'right' + inclusive : {'left', 'right', 'both', 'neither'}, default 'right' Whether the intervals are closed on the left-side, right-side, both or neither. copy : bool, default False @@ -405,7 +412,7 @@ def _from_factorized( """\ Examples -------- - >>> pd.arrays.IntervalArray.from_breaks([0, 1, 2, 3]) + >>> pd.arrays.IntervalArray.from_breaks([0, 1, 2, 3], "right") [(0, 1], (1, 2], (2, 3]] Length: 3, dtype: interval[int64, right] @@ -416,13 +423,15 @@ def _from_factorized( def from_breaks( cls: type[IntervalArrayT], breaks, - closed: IntervalClosedType | None = "right", + inclusive="both", copy: bool = False, dtype: Dtype | None = None, ) -> IntervalArrayT: breaks = _maybe_convert_platform_interval(breaks) - return cls.from_arrays(breaks[:-1], breaks[1:], closed, copy=copy, dtype=dtype) + return cls.from_arrays( + breaks[:-1], breaks[1:], inclusive, copy=copy, dtype=dtype + ) _interval_shared_docs["from_arrays"] = textwrap.dedent( """ @@ -434,7 +443,7 @@ def from_breaks( Left bounds for each interval. right : array-like (1-dimensional) Right bounds for each interval. - closed : {'left', 'right', 'both', 'neither'}, default 'right' + inclusive : {'left', 'right', 'both', 'neither'}, default 'right' Whether the intervals are closed on the left-side, right-side, both or neither. copy : bool, default False @@ -480,7 +489,7 @@ def from_breaks( "klass": "IntervalArray", "examples": textwrap.dedent( """\ - >>> pd.arrays.IntervalArray.from_arrays([0, 1, 2], [1, 2, 3]) + >>> pd.arrays.IntervalArray.from_arrays([0, 1, 2], [1, 2, 3], inclusive="right") [(0, 1], (1, 2], (2, 3]] Length: 3, dtype: interval[int64, right] @@ -492,7 +501,7 @@ def from_arrays( cls: type[IntervalArrayT], left, right, - closed: IntervalClosedType | None = "right", + inclusive="both", copy: bool = False, dtype: Dtype | None = None, ) -> IntervalArrayT: @@ -500,7 +509,12 @@ def from_arrays( right = _maybe_convert_platform_interval(right) return cls._simple_new( - left, right, closed, copy=copy, dtype=dtype, verify_integrity=True + left, + right, + inclusive=inclusive, + copy=copy, + dtype=dtype, + verify_integrity=True, ) _interval_shared_docs["from_tuples"] = textwrap.dedent( @@ -511,7 +525,7 @@ def from_arrays( ---------- data : array-like (1-dimensional) Array of tuples. - closed : {'left', 'right', 'both', 'neither'}, default 'right' + inclusive : {'left', 'right', 'both', 'neither'}, default 'right' Whether the intervals are closed on the left-side, right-side, both or neither. copy : bool, default False @@ -544,7 +558,7 @@ def from_arrays( """\ Examples -------- - >>> pd.arrays.IntervalArray.from_tuples([(0, 1), (1, 2)]) + >>> pd.arrays.IntervalArray.from_tuples([(0, 1), (1, 2)], inclusive="right") [(0, 1], (1, 2]] Length: 2, dtype: interval[int64, right] @@ -555,7 +569,7 @@ def from_arrays( def from_tuples( cls: type[IntervalArrayT], data, - closed="right", + inclusive="both", copy: bool = False, dtype: Dtype | None = None, ) -> IntervalArrayT: @@ -582,7 +596,7 @@ def from_tuples( left.append(lhs) right.append(rhs) - return cls.from_arrays(left, right, closed, copy=False, dtype=dtype) + return cls.from_arrays(left, right, inclusive, copy=False, dtype=dtype) def _validate(self): """ @@ -590,13 +604,13 @@ def _validate(self): Checks that - * closed is valid + * inclusive is valid * left and right match lengths * left and right have the same missing values * left is always below right """ - if self.closed not in VALID_CLOSED: - msg = f"invalid option for 'closed': {self.closed}" + if self.inclusive not in VALID_CLOSED: + msg = f"invalid option for 'inclusive': {self.inclusive}" raise ValueError(msg) if len(self._left) != len(self._right): msg = "left and right must have the same length" @@ -624,7 +638,9 @@ def _shallow_copy(self: IntervalArrayT, left, right) -> IntervalArrayT: right : Index Values to be used for the right-side of the intervals. """ - return self._simple_new(left, right, closed=self.closed, verify_integrity=False) + return self._simple_new( + left, right, inclusive=self.inclusive, verify_integrity=False + ) # --------------------------------------------------------------------- # Descriptive @@ -670,7 +686,7 @@ def __getitem__( # scalar if is_scalar(left) and isna(left): return self._fill_value - return Interval(left, right, self.closed) + return Interval(left, right, inclusive=self.inclusive) if np.ndim(left) > 1: # GH#30588 multi-dimensional indexer disallowed raise ValueError("multi-dimensional indexing not allowed") @@ -711,7 +727,7 @@ def _cmp_method(self, other, op): # extract intervals if we have interval categories with matching closed if is_interval_dtype(other_dtype): - if self.closed != other.categories.closed: + if self.inclusive != other.categories.inclusive: return invalid_comparison(self, other, op) other = other.categories.take( @@ -720,7 +736,7 @@ def _cmp_method(self, other, op): # interval-like -> need same closed and matching endpoints if is_interval_dtype(other_dtype): - if self.closed != other.closed: + if self.inclusive != other.inclusive: return invalid_comparison(self, other, op) elif not isinstance(other, Interval): other = type(self)(other) @@ -936,7 +952,7 @@ def equals(self, other) -> bool: return False return bool( - self.closed == other.closed + self.inclusive == other.inclusive and self.left.equals(other.left) and self.right.equals(other.right) ) @@ -956,14 +972,14 @@ def _concat_same_type( ------- IntervalArray """ - closed_set = {interval.closed for interval in to_concat} - if len(closed_set) != 1: + inclusive_set = {interval.inclusive for interval in to_concat} + if len(inclusive_set) != 1: raise ValueError("Intervals must all be closed on the same side.") - closed = closed_set.pop() + inclusive = inclusive_set.pop() left = np.concatenate([interval.left for interval in to_concat]) right = np.concatenate([interval.right for interval in to_concat]) - return cls._simple_new(left, right, closed=closed, copy=False) + return cls._simple_new(left, right, inclusive=inclusive, copy=False) def copy(self: IntervalArrayT) -> IntervalArrayT: """ @@ -975,9 +991,9 @@ def copy(self: IntervalArrayT) -> IntervalArrayT: """ left = self._left.copy() right = self._right.copy() - closed = self.closed + inclusive = self.inclusive # TODO: Could skip verify_integrity here. - return type(self).from_arrays(left, right, closed=closed) + return type(self).from_arrays(left, right, inclusive=inclusive) def isna(self) -> np.ndarray: return isna(self._left) @@ -999,7 +1015,7 @@ def shift(self, periods: int = 1, fill_value: object = None) -> IntervalArray: from pandas import Index fill_value = Index(self._left, copy=False)._na_value - empty = IntervalArray.from_breaks([fill_value] * (empty_len + 1)) + empty = IntervalArray.from_breaks([fill_value] * (empty_len + 1), "right") else: empty = self._from_sequence([fill_value] * empty_len) @@ -1129,7 +1145,7 @@ def _validate_setitem_value(self, value): value_left, value_right = value, value elif isinstance(value, Interval): - # scalar interval + # scalar self._check_closed_matches(value, name="value") value_left, value_right = value.left, value.right self.left._validate_fill_value(value_left) @@ -1257,7 +1273,7 @@ def mid(self) -> Index: """ Check elementwise if an Interval overlaps the values in the %(klass)s. - Two intervals overlap if they share a common point, including closed + Two intervals overlap if they share a common point, including inclusive endpoints. Intervals that only have an open endpoint in common do not overlap. @@ -1281,14 +1297,14 @@ def mid(self) -> Index: >>> intervals.overlaps(pd.Interval(0.5, 1.5)) array([ True, True, False]) - Intervals that share closed endpoints overlap: + Intervals that share inclusive endpoints overlap: - >>> intervals.overlaps(pd.Interval(1, 3, closed='left')) + >>> intervals.overlaps(pd.Interval(1, 3, inclusive='left')) array([ True, True, True]) Intervals that only have an open endpoint in common do not overlap: - >>> intervals.overlaps(pd.Interval(1, 2, closed='right')) + >>> intervals.overlaps(pd.Interval(1, 2, inclusive='right')) array([False, True, False]) """ ) @@ -1300,7 +1316,7 @@ def mid(self) -> Index: "examples": textwrap.dedent( """\ >>> data = [(0, 1), (1, 3), (2, 4)] - >>> intervals = pd.arrays.IntervalArray.from_tuples(data) + >>> intervals = pd.arrays.IntervalArray.from_tuples(data, "right") >>> intervals [(0, 1], (1, 3], (2, 4]] @@ -1328,12 +1344,12 @@ def overlaps(self, other): # --------------------------------------------------------------------- @property - def closed(self) -> IntervalClosedType: + def inclusive(self) -> IntervalClosedType: """ Whether the intervals are closed on the left-side, right-side, both or neither. """ - return self.dtype.closed + return self.dtype.inclusive _interval_shared_docs["set_closed"] = textwrap.dedent( """ @@ -1342,7 +1358,7 @@ def closed(self) -> IntervalClosedType: Parameters ---------- - closed : {'left', 'right', 'both', 'neither'} + inclusive : {'left', 'right', 'both', 'neither'} Whether the intervals are closed on the left-side, right-side, both or neither. @@ -1362,7 +1378,7 @@ def closed(self) -> IntervalClosedType: """\ Examples -------- - >>> index = pd.arrays.IntervalArray.from_breaks(range(4)) + >>> index = pd.arrays.IntervalArray.from_breaks(range(4), "right") >>> index [(0, 1], (1, 2], (2, 3]] @@ -1375,13 +1391,18 @@ def closed(self) -> IntervalClosedType: ), } ) - def set_closed(self: IntervalArrayT, closed: IntervalClosedType) -> IntervalArrayT: - if closed not in VALID_CLOSED: - msg = f"invalid option for 'closed': {closed}" + def set_closed( + self: IntervalArrayT, inclusive: IntervalClosedType + ) -> IntervalArrayT: + if inclusive not in VALID_CLOSED: + msg = f"invalid option for 'inclusive': {inclusive}" raise ValueError(msg) return type(self)._simple_new( - left=self._left, right=self._right, closed=closed, verify_integrity=False + left=self._left, + right=self._right, + inclusive=inclusive, + verify_integrity=False, ) _interval_shared_docs[ @@ -1403,15 +1424,15 @@ def is_non_overlapping_monotonic(self) -> bool: # or decreasing (e.g., [-1, 0), [-2, -1), [-3, -2), ...) # we already require left <= right - # strict inequality for closed == 'both'; equality implies overlapping + # strict inequality for inclusive == 'both'; equality implies overlapping # at a point when both sides of intervals are included - if self.closed == "both": + if self.inclusive == "both": return bool( (self._right[:-1] < self._left[1:]).all() or (self._left[:-1] > self._right[1:]).all() ) - # non-strict inequality when closed != 'both'; at least one side is + # non-strict inequality when inclusive != 'both'; at least one side is # not included in the intervals, so equality does not imply overlapping return bool( (self._right[:-1] <= self._left[1:]).all() @@ -1429,14 +1450,14 @@ def __array__(self, dtype: NpDtype | None = None) -> np.ndarray: left = self._left right = self._right mask = self.isna() - closed = self.closed + inclusive = self.inclusive result = np.empty(len(left), dtype=object) for i in range(len(left)): if mask[i]: result[i] = np.nan else: - result[i] = Interval(left[i], right[i], closed) + result[i] = Interval(left[i], right[i], inclusive=inclusive) return result def __arrow_array__(self, type=None): @@ -1454,7 +1475,7 @@ def __arrow_array__(self, type=None): f"Conversion to arrow with subtype '{self.dtype.subtype}' " "is not supported" ) from err - interval_type = ArrowIntervalType(subtype, self.closed) + interval_type = ArrowIntervalType(subtype, self.inclusive) storage_array = pyarrow.StructArray.from_arrays( [ pyarrow.array(self._left, type=subtype, from_pandas=True), @@ -1477,12 +1498,13 @@ def __arrow_array__(self, type=None): if type.equals(interval_type.storage_type): return storage_array elif isinstance(type, ArrowIntervalType): - # ensure we have the same subtype and closed attributes + # ensure we have the same subtype and inclusive attributes if not type.equals(interval_type): raise TypeError( "Not supported to convert IntervalArray to type with " f"different 'subtype' ({self.dtype.subtype} vs {type.subtype}) " - f"and 'closed' ({self.closed} vs {type.closed}) attributes" + f"and 'inclusive' ({self.inclusive} vs {type.inclusive}) " + f"attributes" ) else: raise TypeError( @@ -1610,7 +1632,8 @@ def repeat( "klass": "IntervalArray", "examples": textwrap.dedent( """\ - >>> intervals = pd.arrays.IntervalArray.from_tuples([(0, 1), (1, 3), (2, 4)]) + >>> intervals = pd.arrays.IntervalArray.from_tuples([(0, 1), (1, 3), (2, 4)] + ... , "right") >>> intervals [(0, 1], (1, 3], (2, 4]] @@ -1633,7 +1656,7 @@ def isin(self, values) -> np.ndarray: values = extract_array(values, extract_numpy=True) if is_interval_dtype(values.dtype): - if self.closed != values.closed: + if self.inclusive != values.inclusive: # not comparable -> no overlap return np.zeros(self.shape, dtype=bool) diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index 88a92ea1455d0..c6a4effac7a37 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -515,7 +515,7 @@ def ensure_dtype_can_hold_na(dtype: DtypeObj) -> DtypeObj: elif isinstance(dtype, IntervalDtype): # TODO(GH#45349): don't special-case IntervalDtype, allow # overriding instead of returning object below. - return IntervalDtype(np.float64, closed=dtype.closed) + return IntervalDtype(np.float64, inclusive=dtype.inclusive) return _dtype_obj elif dtype.kind == "b": return _dtype_obj @@ -834,7 +834,7 @@ def infer_dtype_from_scalar(val, pandas_dtype: bool = False) -> tuple[DtypeObj, dtype = PeriodDtype(freq=val.freq) elif lib.is_interval(val): subtype = infer_dtype_from_scalar(val.left, pandas_dtype=True)[0] - dtype = IntervalDtype(subtype=subtype, closed=val.closed) + dtype = IntervalDtype(subtype=subtype, inclusive=val.inclusive) return dtype, val diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 6776064342db0..a192337daf59b 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -479,7 +479,7 @@ def is_interval_dtype(arr_or_dtype) -> bool: >>> is_interval_dtype([1, 2, 3]) False >>> - >>> interval = pd.Interval(1, 2, closed="right") + >>> interval = pd.Interval(1, 2, inclusive="right") >>> is_interval_dtype(interval) False >>> is_interval_dtype(pd.IntervalIndex([interval])) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 58e91f46dff43..64d46976b54f6 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -14,8 +14,14 @@ import numpy as np import pytz -from pandas._libs import missing as libmissing -from pandas._libs.interval import Interval +from pandas._libs import ( + lib, + missing as libmissing, +) +from pandas._libs.interval import ( + Interval, + _warning_interval, +) from pandas._libs.properties import cache_readonly from pandas._libs.tslibs import ( BaseOffset, @@ -1040,7 +1046,7 @@ class IntervalDtype(PandasExtensionDtype): Examples -------- - >>> pd.IntervalDtype(subtype='int64', closed='both') + >>> pd.IntervalDtype(subtype='int64', inclusive='both') interval[int64, both] """ @@ -1051,27 +1057,42 @@ class IntervalDtype(PandasExtensionDtype): num = 103 _metadata = ( "subtype", - "closed", + "inclusive", ) _match = re.compile( - r"(I|i)nterval\[(?P[^,]+)(, (?P(right|left|both|neither)))?\]" + r"(I|i)nterval\[(?P[^,]+)(, (" + r"?P(right|left|both|neither)))?\]" ) _cache_dtypes: dict[str_type, PandasExtensionDtype] = {} - def __new__(cls, subtype=None, closed: str_type | None = None): + def __new__( + cls, + subtype=None, + inclusive: str_type | None = None, + closed: None | lib.NoDefault = lib.no_default, + ): from pandas.core.dtypes.common import ( is_string_dtype, pandas_dtype, ) - if closed is not None and closed not in {"right", "left", "both", "neither"}: - raise ValueError("closed must be one of 'right', 'left', 'both', 'neither'") + inclusive, closed = _warning_interval(inclusive, closed) + + if inclusive is not None and inclusive not in { + "right", + "left", + "both", + "neither", + }: + raise ValueError( + "inclusive must be one of 'right', 'left', 'both', 'neither'" + ) if isinstance(subtype, IntervalDtype): - if closed is not None and closed != subtype.closed: + if inclusive is not None and inclusive != subtype.inclusive: raise ValueError( - "dtype.closed and 'closed' do not match. " - "Try IntervalDtype(dtype.subtype, closed) instead." + "dtype.inclusive and 'inclusive' do not match. " + "Try IntervalDtype(dtype.subtype, inclusive) instead." ) return subtype elif subtype is None: @@ -1079,7 +1100,7 @@ def __new__(cls, subtype=None, closed: str_type | None = None): # generally for pickle compat u = object.__new__(cls) u._subtype = None - u._closed = closed + u._closed = inclusive return u elif isinstance(subtype, str) and subtype.lower() == "interval": subtype = None @@ -1089,14 +1110,14 @@ def __new__(cls, subtype=None, closed: str_type | None = None): if m is not None: gd = m.groupdict() subtype = gd["subtype"] - if gd.get("closed", None) is not None: - if closed is not None: - if closed != gd["closed"]: + if gd.get("inclusive", None) is not None: + if inclusive is not None: + if inclusive != gd["inclusive"]: raise ValueError( - "'closed' keyword does not match value " + "'inclusive' keyword does not match value " "specified in dtype string" ) - closed = gd["closed"] + inclusive = gd["inclusive"] try: subtype = pandas_dtype(subtype) @@ -1111,13 +1132,13 @@ def __new__(cls, subtype=None, closed: str_type | None = None): ) raise TypeError(msg) - key = str(subtype) + str(closed) + key = str(subtype) + str(inclusive) try: return cls._cache_dtypes[key] except KeyError: u = object.__new__(cls) u._subtype = subtype - u._closed = closed + u._closed = inclusive cls._cache_dtypes[key] = u return u @@ -1134,7 +1155,7 @@ def _can_hold_na(self) -> bool: return True @property - def closed(self): + def inclusive(self): return self._closed @property @@ -1186,10 +1207,10 @@ def type(self): def __str__(self) -> str_type: if self.subtype is None: return "interval" - if self.closed is None: + if self.inclusive is None: # Only partially initialized GH#38394 return f"interval[{self.subtype}]" - return f"interval[{self.subtype}, {self.closed}]" + return f"interval[{self.subtype}, {self.inclusive}]" def __hash__(self) -> int: # make myself hashable @@ -1203,7 +1224,7 @@ def __eq__(self, other: Any) -> bool: elif self.subtype is None or other.subtype is None: # None should match any subtype return True - elif self.closed != other.closed: + elif self.inclusive != other.inclusive: return False else: from pandas.core.dtypes.common import is_dtype_equal @@ -1215,9 +1236,8 @@ def __setstate__(self, state): # PandasExtensionDtype superclass and uses the public properties to # pickle -> need to set the settable private ones here (see GH26067) self._subtype = state["subtype"] - - # backward-compat older pickles won't have "closed" key - self._closed = state.pop("closed", None) + # backward-compat older pickles won't have "inclusive" key + self._closed = state.pop("inclusive", None) @classmethod def is_dtype(cls, dtype: object) -> bool: @@ -1259,14 +1279,14 @@ def __from_arrow__( arr = arr.storage left = np.asarray(arr.field("left"), dtype=self.subtype) right = np.asarray(arr.field("right"), dtype=self.subtype) - iarr = IntervalArray.from_arrays(left, right, closed=self.closed) + iarr = IntervalArray.from_arrays(left, right, inclusive=self.inclusive) results.append(iarr) if not results: return IntervalArray.from_arrays( np.array([], dtype=self.subtype), np.array([], dtype=self.subtype), - closed=self.closed, + inclusive=self.inclusive, ) return IntervalArray._concat_same_type(results) @@ -1274,8 +1294,8 @@ def _get_common_dtype(self, dtypes: list[DtypeObj]) -> DtypeObj | None: if not all(isinstance(x, IntervalDtype) for x in dtypes): return None - closed = cast("IntervalDtype", dtypes[0]).closed - if not all(cast("IntervalDtype", x).closed == closed for x in dtypes): + inclusive = cast("IntervalDtype", dtypes[0]).inclusive + if not all(cast("IntervalDtype", x).inclusive == inclusive for x in dtypes): return np.dtype(object) from pandas.core.dtypes.cast import find_common_type @@ -1283,7 +1303,7 @@ def _get_common_dtype(self, dtypes: list[DtypeObj]) -> DtypeObj | None: common = find_common_type([cast("IntervalDtype", x).subtype for x in dtypes]) if common == object: return np.dtype(object) - return IntervalDtype(common, closed=closed) + return IntervalDtype(common, inclusive=inclusive) class PandasDtype(ExtensionDtype): diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index a89b52e0950f2..11e2da47c5738 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -11,7 +11,6 @@ Hashable, Literal, ) -import warnings import numpy as np @@ -20,6 +19,7 @@ Interval, IntervalMixin, IntervalTree, + _warning_interval, ) from pandas._libs.tslibs import ( BaseOffset, @@ -189,12 +189,12 @@ def _new_IntervalIndex(cls, d): ], IntervalArray, ) -@inherit_names(["is_non_overlapping_monotonic", "closed"], IntervalArray, cache=True) +@inherit_names(["is_non_overlapping_monotonic", "inclusive"], IntervalArray, cache=True) class IntervalIndex(ExtensionIndex): _typ = "intervalindex" # annotate properties pinned via inherit_names - closed: IntervalClosedType + inclusive: IntervalClosedType is_non_overlapping_monotonic: bool closed_left: bool closed_right: bool @@ -212,19 +212,22 @@ class IntervalIndex(ExtensionIndex): def __new__( cls, data, - closed=None, + inclusive=None, + closed: None | lib.NoDefault = lib.no_default, dtype: Dtype | None = None, copy: bool = False, name: Hashable = None, verify_integrity: bool = True, ) -> IntervalIndex: + inclusive, closed = _warning_interval(inclusive, closed) + name = maybe_extract_name(name, data, cls) with rewrite_exception("IntervalArray", cls.__name__): array = IntervalArray( data, - closed=closed, + inclusive=inclusive, copy=copy, dtype=dtype, verify_integrity=verify_integrity, @@ -241,7 +244,7 @@ def __new__( """\ Examples -------- - >>> pd.IntervalIndex.from_breaks([0, 1, 2, 3]) + >>> pd.IntervalIndex.from_breaks([0, 1, 2, 3], "right") IntervalIndex([(0, 1], (1, 2], (2, 3]], dtype='interval[int64, right]') """ @@ -251,14 +254,20 @@ def __new__( def from_breaks( cls, breaks, - closed: IntervalClosedType | None = "right", + inclusive=None, + closed: None | lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, ) -> IntervalIndex: + + inclusive, closed = _warning_interval(inclusive, closed) + if inclusive is None: + inclusive = "both" + with rewrite_exception("IntervalArray", cls.__name__): array = IntervalArray.from_breaks( - breaks, closed=closed, copy=copy, dtype=dtype + breaks, inclusive=inclusive, copy=copy, dtype=dtype ) return cls._simple_new(array, name=name) @@ -271,7 +280,7 @@ def from_breaks( """\ Examples -------- - >>> pd.IntervalIndex.from_arrays([0, 1, 2], [1, 2, 3]) + >>> pd.IntervalIndex.from_arrays([0, 1, 2], [1, 2, 3], "right") IntervalIndex([(0, 1], (1, 2], (2, 3]], dtype='interval[int64, right]') """ @@ -282,14 +291,20 @@ def from_arrays( cls, left, right, - closed: IntervalClosedType = "right", + inclusive=None, + closed: None | lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, ) -> IntervalIndex: + + inclusive, closed = _warning_interval(inclusive, closed) + if inclusive is None: + inclusive = "both" + with rewrite_exception("IntervalArray", cls.__name__): array = IntervalArray.from_arrays( - left, right, closed, copy=copy, dtype=dtype + left, right, inclusive, copy=copy, dtype=dtype ) return cls._simple_new(array, name=name) @@ -302,7 +317,7 @@ def from_arrays( """\ Examples -------- - >>> pd.IntervalIndex.from_tuples([(0, 1), (1, 2)]) + >>> pd.IntervalIndex.from_tuples([(0, 1), (1, 2)], "right") IntervalIndex([(0, 1], (1, 2]], dtype='interval[int64, right]') """ @@ -312,13 +327,21 @@ def from_arrays( def from_tuples( cls, data, - closed: str = "right", + inclusive=None, + closed: None | lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, ) -> IntervalIndex: + + inclusive, closed = _warning_interval(inclusive, closed) + if inclusive is None: + inclusive = "both" + with rewrite_exception("IntervalArray", cls.__name__): - arr = IntervalArray.from_tuples(data, closed=closed, copy=copy, dtype=dtype) + arr = IntervalArray.from_tuples( + data, inclusive=inclusive, copy=copy, dtype=dtype + ) return cls._simple_new(arr, name=name) # -------------------------------------------------------------------- @@ -328,7 +351,7 @@ def from_tuples( def _engine(self) -> IntervalTree: # type: ignore[override] left = self._maybe_convert_i8(self.left) right = self._maybe_convert_i8(self.right) - return IntervalTree(left, right, closed=self.closed) + return IntervalTree(left, right, inclusive=self.inclusive) def __contains__(self, key: Any) -> bool: """ @@ -363,7 +386,7 @@ def __reduce__(self): d = { "left": self.left, "right": self.right, - "closed": self.closed, + "inclusive": self.inclusive, "name": self.name, } return _new_IntervalIndex, (type(self), d), None @@ -418,7 +441,7 @@ def is_overlapping(self) -> bool: """ Return True if the IntervalIndex has overlapping intervals, else False. - Two intervals overlap if they share a common point, including closed + Two intervals overlap if they share a common point, including inclusive endpoints. Intervals that only have an open endpoint in common do not overlap. @@ -435,7 +458,7 @@ def is_overlapping(self) -> bool: Examples -------- - >>> index = pd.IntervalIndex.from_tuples([(0, 2), (1, 3), (4, 5)]) + >>> index = pd.IntervalIndex.from_tuples([(0, 2), (1, 3), (4, 5)], "right") >>> index IntervalIndex([(0, 2], (1, 3], (4, 5]], dtype='interval[int64, right]') @@ -519,7 +542,7 @@ def _maybe_convert_i8(self, key): constructor = Interval if scalar else IntervalIndex.from_arrays # error: "object" not callable return constructor( - left, right, closed=self.closed + left, right, inclusive=self.inclusive ) # type: ignore[operator] if scalar: @@ -600,7 +623,7 @@ def get_loc( Examples -------- >>> i1, i2 = pd.Interval(0, 1), pd.Interval(1, 2) - >>> index = pd.IntervalIndex([i1, i2]) + >>> index = pd.IntervalIndex([i1, i2], "right") >>> index.get_loc(1) 0 @@ -613,20 +636,20 @@ def get_loc( relevant intervals. >>> i3 = pd.Interval(0, 2) - >>> overlapping_index = pd.IntervalIndex([i1, i2, i3]) + >>> overlapping_index = pd.IntervalIndex([i1, i2, i3], "right") >>> overlapping_index.get_loc(0.5) array([ True, False, True]) Only exact matches will be returned if an interval is provided. - >>> index.get_loc(pd.Interval(0, 1)) + >>> index.get_loc(pd.Interval(0, 1, "right")) 0 """ self._check_indexing_method(method) self._check_indexing_error(key) if isinstance(key, Interval): - if self.closed != key.closed: + if self.inclusive != key.inclusive: raise KeyError(key) mask = (self.left == key.left) & (self.right == key.right) elif is_valid_na_for_dtype(key, self.dtype): @@ -687,7 +710,7 @@ def get_indexer_non_unique( target = ensure_index(target) if not self._should_compare(target) and not self._should_partial_index(target): - # e.g. IntervalIndex with different closed or incompatible subtype + # e.g. IntervalIndex with different inclusive or incompatible subtype # -> no matches return self._get_indexer_non_comparable(target, None, unique=False) @@ -839,7 +862,7 @@ def _intersection(self, other, sort): """ intersection specialized to the case with matching dtypes. """ - # For IntervalIndex we also know other.closed == self.closed + # For IntervalIndex we also know other.inclusive == self.inclusive if self.left.is_unique and self.right.is_unique: taken = self._intersection_unique(other) elif other.left.is_unique and other.right.is_unique and self.isna().sum() <= 1: @@ -1054,27 +1077,8 @@ def interval_range( IntervalIndex([[1, 2], [2, 3], [3, 4], [4, 5]], dtype='interval[int64, both]') """ - if inclusive is not None and closed is not lib.no_default: - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif closed is not lib.no_default: - warnings.warn( - "Argument `closed` is deprecated in favor of `inclusive`.", - FutureWarning, - stacklevel=2, - ) - if closed is None: - inclusive = "both" - elif closed in ("both", "neither", "left", "right"): - inclusive = closed - else: - raise ValueError( - "Argument `closed` has to be either" - "'both', 'neither', 'left' or 'right'" - ) - elif inclusive is None: + inclusive, closed = _warning_interval(inclusive, closed) + if inclusive is None: inclusive = "both" start = maybe_box_datetimelike(start) @@ -1149,4 +1153,4 @@ def interval_range( else: breaks = timedelta_range(start=start, end=end, periods=periods, freq=freq) - return IntervalIndex.from_breaks(breaks, name=name, closed=inclusive) + return IntervalIndex.from_breaks(breaks, name=name, inclusive=inclusive) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 71e2a1e36cbbf..3836f3e6540b4 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -1961,8 +1961,8 @@ def _catch_deprecated_value_error(err: Exception) -> None: # is enforced, stop catching ValueError here altogether if isinstance(err, IncompatibleFrequency): pass - elif "'value.closed' is" in str(err): - # IntervalDtype mismatched 'closed' + elif "'value.inclusive' is" in str(err): + # IntervalDtype mismatched 'inclusive' pass elif "Timezones don't match" not in str(err): raise diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index 94705790e40bd..00b2b30eb3122 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -231,7 +231,7 @@ def cut( is to the left of the first bin (which is closed on the right), and 1.5 falls between two bins. - >>> bins = pd.IntervalIndex.from_tuples([(0, 1), (2, 3), (4, 5)]) + >>> bins = pd.IntervalIndex.from_tuples([(0, 1), (2, 3), (4, 5)], inclusive="right") >>> pd.cut([0, 0.5, 1.5, 2.5, 4.5], bins) [NaN, (0.0, 1.0], NaN, (2.0, 3.0], (4.0, 5.0]] Categories (3, interval[int64, right]): [(0, 1] < (2, 3] < (4, 5]] @@ -561,7 +561,7 @@ def _format_labels( bins, precision: int, right: bool = True, include_lowest: bool = False, dtype=None ): """based on the dtype, return our labels""" - closed: IntervalLeftRight = "right" if right else "left" + inclusive: IntervalLeftRight = "right" if right else "left" formatter: Callable[[Any], Timestamp] | Callable[[Any], Timedelta] @@ -584,7 +584,7 @@ def _format_labels( # adjust lhs of first interval by precision to account for being right closed breaks[0] = adjust(breaks[0]) - return IntervalIndex.from_breaks(breaks, closed=closed) + return IntervalIndex.from_breaks(breaks, inclusive=inclusive) def _preprocess_for_cut(x): diff --git a/pandas/tests/arithmetic/test_interval.py b/pandas/tests/arithmetic/test_interval.py index 88e3dca62d9e0..99e1ad1767e07 100644 --- a/pandas/tests/arithmetic/test_interval.py +++ b/pandas/tests/arithmetic/test_interval.py @@ -62,16 +62,16 @@ def interval_array(left_right_dtypes): return IntervalArray.from_arrays(left, right) -def create_categorical_intervals(left, right, closed="right"): - return Categorical(IntervalIndex.from_arrays(left, right, closed)) +def create_categorical_intervals(left, right, inclusive="right"): + return Categorical(IntervalIndex.from_arrays(left, right, inclusive)) -def create_series_intervals(left, right, closed="right"): - return Series(IntervalArray.from_arrays(left, right, closed)) +def create_series_intervals(left, right, inclusive="right"): + return Series(IntervalArray.from_arrays(left, right, inclusive)) -def create_series_categorical_intervals(left, right, closed="right"): - return Series(Categorical(IntervalIndex.from_arrays(left, right, closed))) +def create_series_categorical_intervals(left, right, inclusive="right"): + return Series(Categorical(IntervalIndex.from_arrays(left, right, inclusive))) class TestComparison: @@ -126,8 +126,10 @@ def test_compare_scalar_interval(self, op, interval_array): tm.assert_numpy_array_equal(result, expected) def test_compare_scalar_interval_mixed_closed(self, op, closed, other_closed): - interval_array = IntervalArray.from_arrays(range(2), range(1, 3), closed=closed) - other = Interval(0, 1, closed=other_closed) + interval_array = IntervalArray.from_arrays( + range(2), range(1, 3), inclusive=closed + ) + other = Interval(0, 1, inclusive=other_closed) result = op(interval_array, other) expected = self.elementwise_comparison(op, interval_array, other) @@ -207,8 +209,10 @@ def test_compare_list_like_interval(self, op, interval_array, interval_construct def test_compare_list_like_interval_mixed_closed( self, op, interval_constructor, closed, other_closed ): - interval_array = IntervalArray.from_arrays(range(2), range(1, 3), closed=closed) - other = interval_constructor(range(2), range(1, 3), closed=other_closed) + interval_array = IntervalArray.from_arrays( + range(2), range(1, 3), inclusive=closed + ) + other = interval_constructor(range(2), range(1, 3), inclusive=other_closed) result = op(interval_array, other) expected = self.elementwise_comparison(op, interval_array, other) diff --git a/pandas/tests/arrays/interval/test_interval.py b/pandas/tests/arrays/interval/test_interval.py index eaf86f5d521ae..28ffd5b4caf98 100644 --- a/pandas/tests/arrays/interval/test_interval.py +++ b/pandas/tests/arrays/interval/test_interval.py @@ -55,7 +55,7 @@ def test_is_empty(self, constructor, left, right, closed): # GH27219 tuples = [(left, left), (left, right), np.nan] expected = np.array([closed != "both", False, False]) - result = constructor.from_tuples(tuples, closed=closed).is_empty + result = constructor.from_tuples(tuples, inclusive=closed).is_empty tm.assert_numpy_array_equal(result, expected) @@ -63,23 +63,23 @@ class TestMethods: @pytest.mark.parametrize("new_closed", ["left", "right", "both", "neither"]) def test_set_closed(self, closed, new_closed): # GH 21670 - array = IntervalArray.from_breaks(range(10), closed=closed) + array = IntervalArray.from_breaks(range(10), inclusive=closed) result = array.set_closed(new_closed) - expected = IntervalArray.from_breaks(range(10), closed=new_closed) + expected = IntervalArray.from_breaks(range(10), inclusive=new_closed) tm.assert_extension_array_equal(result, expected) @pytest.mark.parametrize( "other", [ - Interval(0, 1, closed="right"), - IntervalArray.from_breaks([1, 2, 3, 4], closed="right"), + Interval(0, 1, inclusive="right"), + IntervalArray.from_breaks([1, 2, 3, 4], inclusive="right"), ], ) def test_where_raises(self, other): # GH#45768 The IntervalArray methods raises; the Series method coerces - ser = pd.Series(IntervalArray.from_breaks([1, 2, 3, 4], closed="left")) + ser = pd.Series(IntervalArray.from_breaks([1, 2, 3, 4], inclusive="left")) mask = np.array([True, False, True]) - match = "'value.closed' is 'right', expected 'left'." + match = "'value.inclusive' is 'right', expected 'left'." with pytest.raises(ValueError, match=match): ser.array._where(mask, other) @@ -89,15 +89,15 @@ def test_where_raises(self, other): def test_shift(self): # https://github.com/pandas-dev/pandas/issues/31495, GH#22428, GH#31502 - a = IntervalArray.from_breaks([1, 2, 3]) + a = IntervalArray.from_breaks([1, 2, 3], "right") result = a.shift() # int -> float - expected = IntervalArray.from_tuples([(np.nan, np.nan), (1.0, 2.0)]) + expected = IntervalArray.from_tuples([(np.nan, np.nan), (1.0, 2.0)], "right") tm.assert_interval_array_equal(result, expected) def test_shift_datetime(self): # GH#31502, GH#31504 - a = IntervalArray.from_breaks(date_range("2000", periods=4)) + a = IntervalArray.from_breaks(date_range("2000", periods=4), "right") result = a.shift(2) expected = a.take([-1, -1, 0], allow_fill=True) tm.assert_interval_array_equal(result, expected) @@ -135,11 +135,11 @@ def test_set_na(self, left_right_dtypes): tm.assert_extension_array_equal(result, expected) def test_setitem_mismatched_closed(self): - arr = IntervalArray.from_breaks(range(4)) + arr = IntervalArray.from_breaks(range(4), "right") orig = arr.copy() other = arr.set_closed("both") - msg = "'value.closed' is 'both', expected 'right'" + msg = "'value.inclusive' is 'both', expected 'right'" with pytest.raises(ValueError, match=msg): arr[0] = other[0] with pytest.raises(ValueError, match=msg): @@ -156,13 +156,13 @@ def test_setitem_mismatched_closed(self): arr[:] = other[::-1].astype("category") # empty list should be no-op - arr[:0] = [] + arr[:0] = IntervalArray.from_breaks([], "right") tm.assert_interval_array_equal(arr, orig) def test_repr(): # GH 25022 - arr = IntervalArray.from_tuples([(0, 1), (1, 2)]) + arr = IntervalArray.from_tuples([(0, 1), (1, 2)], "right") result = repr(arr) expected = ( "\n" @@ -254,7 +254,7 @@ def test_arrow_extension_type(): p2 = ArrowIntervalType(pa.int64(), "left") p3 = ArrowIntervalType(pa.int64(), "right") - assert p1.closed == "left" + assert p1.inclusive == "left" assert p1 == p2 assert not p1 == p3 assert hash(p1) == hash(p2) @@ -271,7 +271,7 @@ def test_arrow_array(): result = pa.array(intervals) assert isinstance(result.type, ArrowIntervalType) - assert result.type.closed == intervals.closed + assert result.type.inclusive == intervals.inclusive assert result.type.subtype == pa.int64() assert result.storage.field("left").equals(pa.array([1, 2, 3, 4], type="int64")) assert result.storage.field("right").equals(pa.array([2, 3, 4, 5], type="int64")) @@ -302,7 +302,7 @@ def test_arrow_array_missing(): result = pa.array(arr) assert isinstance(result.type, ArrowIntervalType) - assert result.type.closed == arr.closed + assert result.type.inclusive == arr.inclusive assert result.type.subtype == pa.float64() # fields have missing values (not NaN) @@ -386,13 +386,60 @@ def test_from_arrow_from_raw_struct_array(): import pyarrow as pa arr = pa.array([{"left": 0, "right": 1}, {"left": 1, "right": 2}]) - dtype = pd.IntervalDtype(np.dtype("int64"), closed="neither") + dtype = pd.IntervalDtype(np.dtype("int64"), inclusive="neither") result = dtype.__from_arrow__(arr) expected = IntervalArray.from_breaks( - np.array([0, 1, 2], dtype="int64"), closed="neither" + np.array([0, 1, 2], dtype="int64"), inclusive="neither" ) tm.assert_extension_array_equal(result, expected) result = dtype.__from_arrow__(pa.chunked_array([arr])) tm.assert_extension_array_equal(result, expected) + + +def test_interval_error_and_warning(): + # GH 40245 + msg = ( + "Deprecated argument `closed` cannot " + "be passed if argument `inclusive` is not None" + ) + with pytest.raises(ValueError, match=msg): + Interval(0, 1, closed="both", inclusive="both") + + msg = "Argument `closed` is deprecated in favor of `inclusive`" + with tm.assert_produces_warning(FutureWarning, match=msg, check_stacklevel=False): + Interval(0, 1, closed="both") + + +def test_interval_array_error_and_warning(): + # GH 40245 + msg = ( + "Deprecated argument `closed` cannot " + "be passed if argument `inclusive` is not None" + ) + with pytest.raises(ValueError, match=msg): + IntervalArray([Interval(0, 1), Interval(1, 5)], closed="both", inclusive="both") + + msg = "Argument `closed` is deprecated in favor of `inclusive`" + with tm.assert_produces_warning(FutureWarning, match=msg, check_stacklevel=False): + IntervalArray([Interval(0, 1), Interval(1, 5)], closed="both") + + +@pyarrow_skip +def test_arrow_interval_type_error_and_warning(): + # GH 40245 + import pyarrow as pa + + from pandas.core.arrays.arrow._arrow_utils import ArrowIntervalType + + msg = ( + "Deprecated argument `closed` cannot " + "be passed if argument `inclusive` is not None" + ) + with pytest.raises(ValueError, match=msg): + ArrowIntervalType(pa.int64(), closed="both", inclusive="both") + + msg = "Argument `closed` is deprecated in favor of `inclusive`" + with tm.assert_produces_warning(FutureWarning, match=msg, check_stacklevel=False): + ArrowIntervalType(pa.int64(), closed="both") diff --git a/pandas/tests/arrays/test_array.py b/pandas/tests/arrays/test_array.py index 3c8d12556bf1c..bf7dca9e1d5a0 100644 --- a/pandas/tests/arrays/test_array.py +++ b/pandas/tests/arrays/test_array.py @@ -133,9 +133,9 @@ ), # Interval ( - [pd.Interval(1, 2), pd.Interval(3, 4)], + [pd.Interval(1, 2, "right"), pd.Interval(3, 4, "right")], "interval", - IntervalArray.from_tuples([(1, 2), (3, 4)]), + IntervalArray.from_tuples([(1, 2), (3, 4)], "right"), ), # Sparse ([0, 1], "Sparse[int64]", SparseArray([0, 1], dtype="int64")), @@ -206,7 +206,10 @@ def test_array_copy(): period_array(["2000", "2001"], freq="D"), ), # interval - ([pd.Interval(0, 1), pd.Interval(1, 2)], IntervalArray.from_breaks([0, 1, 2])), + ( + [pd.Interval(0, 1, "right"), pd.Interval(1, 2, "right")], + IntervalArray.from_breaks([0, 1, 2], "right"), + ), # datetime ( [pd.Timestamp("2000"), pd.Timestamp("2001")], @@ -296,7 +299,7 @@ def test_array_inference(data, expected): # mix of frequencies [pd.Period("2000", "D"), pd.Period("2001", "A")], # mix of closed - [pd.Interval(0, 1, closed="left"), pd.Interval(1, 2, closed="right")], + [pd.Interval(0, 1, "left"), pd.Interval(1, 2, "right")], # Mix of timezones [pd.Timestamp("2000", tz="CET"), pd.Timestamp("2000", tz="UTC")], # Mix of tz-aware and tz-naive diff --git a/pandas/tests/base/test_conversion.py b/pandas/tests/base/test_conversion.py index 599aaae4d3527..3adaddf89cf30 100644 --- a/pandas/tests/base/test_conversion.py +++ b/pandas/tests/base/test_conversion.py @@ -290,8 +290,10 @@ def test_array_multiindex_raises(): ), (pd.array([0, np.nan], dtype="Int64"), np.array([0, pd.NA], dtype=object)), ( - IntervalArray.from_breaks([0, 1, 2]), - np.array([pd.Interval(0, 1), pd.Interval(1, 2)], dtype=object), + IntervalArray.from_breaks([0, 1, 2], "right"), + np.array( + [pd.Interval(0, 1, "right"), pd.Interval(1, 2, "right")], dtype=object + ), ), (SparseArray([0, 1]), np.array([0, 1], dtype=np.int64)), # tz-naive datetime diff --git a/pandas/tests/base/test_value_counts.py b/pandas/tests/base/test_value_counts.py index c46f1b036dbee..55a6cc48ebfc8 100644 --- a/pandas/tests/base/test_value_counts.py +++ b/pandas/tests/base/test_value_counts.py @@ -133,10 +133,10 @@ def test_value_counts_bins(index_or_series): s1 = Series([1, 1, 2, 3]) res1 = s1.value_counts(bins=1) - exp1 = Series({Interval(0.997, 3.0): 4}) + exp1 = Series({Interval(0.997, 3.0, "right"): 4}) tm.assert_series_equal(res1, exp1) res1n = s1.value_counts(bins=1, normalize=True) - exp1n = Series({Interval(0.997, 3.0): 1.0}) + exp1n = Series({Interval(0.997, 3.0, "right"): 1.0}) tm.assert_series_equal(res1n, exp1n) if isinstance(s1, Index): @@ -149,12 +149,12 @@ def test_value_counts_bins(index_or_series): # these return the same res4 = s1.value_counts(bins=4, dropna=True) - intervals = IntervalIndex.from_breaks([0.997, 1.5, 2.0, 2.5, 3.0]) + intervals = IntervalIndex.from_breaks([0.997, 1.5, 2.0, 2.5, 3.0], "right") exp4 = Series([2, 1, 1, 0], index=intervals.take([0, 1, 3, 2])) tm.assert_series_equal(res4, exp4) res4 = s1.value_counts(bins=4, dropna=False) - intervals = IntervalIndex.from_breaks([0.997, 1.5, 2.0, 2.5, 3.0]) + intervals = IntervalIndex.from_breaks([0.997, 1.5, 2.0, 2.5, 3.0], "right") exp4 = Series([2, 1, 1, 0], index=intervals.take([0, 1, 3, 2])) tm.assert_series_equal(res4, exp4) diff --git a/pandas/tests/dtypes/test_common.py b/pandas/tests/dtypes/test_common.py index a32b37fbdd71b..c5d0567b6dfc0 100644 --- a/pandas/tests/dtypes/test_common.py +++ b/pandas/tests/dtypes/test_common.py @@ -269,7 +269,7 @@ def test_is_interval_dtype(): assert com.is_interval_dtype(IntervalDtype()) - interval = pd.Interval(1, 2, closed="right") + interval = pd.Interval(1, 2, inclusive="right") assert not com.is_interval_dtype(interval) assert com.is_interval_dtype(pd.IntervalIndex([interval])) diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index f077317e7ebbe..b7de8016f8fac 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -568,10 +568,19 @@ def test_hash_vs_equality(self, dtype): "subtype", ["interval[int64]", "Interval[int64]", "int64", np.dtype("int64")] ) def test_construction(self, subtype): - i = IntervalDtype(subtype, closed="right") + i = IntervalDtype(subtype, inclusive="right") assert i.subtype == np.dtype("int64") assert is_interval_dtype(i) + @pytest.mark.parametrize( + "subtype", ["interval[int64, right]", "Interval[int64, right]"] + ) + def test_construction_string_regex(self, subtype): + i = IntervalDtype(subtype=subtype) + assert i.subtype == np.dtype("int64") + assert i.inclusive == "right" + assert is_interval_dtype(i) + @pytest.mark.parametrize( "subtype", ["interval[int64]", "Interval[int64]", "int64", np.dtype("int64")] ) @@ -579,10 +588,10 @@ def test_construction_allows_closed_none(self, subtype): # GH#38394 dtype = IntervalDtype(subtype) - assert dtype.closed is None + assert dtype.inclusive is None def test_closed_mismatch(self): - msg = "'closed' keyword does not match value specified in dtype string" + msg = "'inclusive' keyword does not match value specified in dtype string" with pytest.raises(ValueError, match=msg): IntervalDtype("interval[int64, left]", "right") @@ -624,12 +633,12 @@ def test_closed_must_match(self): # GH#37933 dtype = IntervalDtype(np.float64, "left") - msg = "dtype.closed and 'closed' do not match" + msg = "dtype.inclusive and 'inclusive' do not match" with pytest.raises(ValueError, match=msg): - IntervalDtype(dtype, closed="both") + IntervalDtype(dtype, inclusive="both") def test_closed_invalid(self): - with pytest.raises(ValueError, match="closed must be one of"): + with pytest.raises(ValueError, match="inclusive must be one of"): IntervalDtype(np.float64, "foo") def test_construction_from_string(self, dtype): @@ -729,8 +738,8 @@ def test_equality(self, dtype): ) def test_equality_generic(self, subtype): # GH 18980 - closed = "right" if subtype is not None else None - dtype = IntervalDtype(subtype, closed=closed) + inclusive = "right" if subtype is not None else None + dtype = IntervalDtype(subtype, inclusive=inclusive) assert is_dtype_equal(dtype, "interval") assert is_dtype_equal(dtype, IntervalDtype()) @@ -748,9 +757,9 @@ def test_equality_generic(self, subtype): ) def test_name_repr(self, subtype): # GH 18980 - closed = "right" if subtype is not None else None - dtype = IntervalDtype(subtype, closed=closed) - expected = f"interval[{subtype}, {closed}]" + inclusive = "right" if subtype is not None else None + dtype = IntervalDtype(subtype, inclusive=inclusive) + expected = f"interval[{subtype}, {inclusive}]" assert str(dtype) == expected assert dtype.name == "interval" @@ -812,6 +821,21 @@ def test_unpickling_without_closed(self): tm.round_trip_pickle(dtype) + def test_interval_dtype_error_and_warning(self): + # GH 40245 + msg = ( + "Deprecated argument `closed` cannot " + "be passed if argument `inclusive` is not None" + ) + with pytest.raises(ValueError, match=msg): + IntervalDtype("int64", closed="right", inclusive="right") + + msg = "Argument `closed` is deprecated in favor of `inclusive`" + with tm.assert_produces_warning( + FutureWarning, match=msg, check_stacklevel=False + ): + IntervalDtype("int64", closed="right") + class TestCategoricalDtypeParametrized: @pytest.mark.parametrize( diff --git a/pandas/tests/dtypes/test_inference.py b/pandas/tests/dtypes/test_inference.py index 15f6e82419049..b12476deccbfc 100644 --- a/pandas/tests/dtypes/test_inference.py +++ b/pandas/tests/dtypes/test_inference.py @@ -959,7 +959,7 @@ def test_mixed_dtypes_remain_object_array(self): @pytest.mark.parametrize( "idx", [ - pd.IntervalIndex.from_breaks(range(5), closed="both"), + pd.IntervalIndex.from_breaks(range(5), inclusive="both"), pd.period_range("2016-01-01", periods=3, freq="D"), ], ) @@ -1652,7 +1652,7 @@ def test_categorical(self): @pytest.mark.parametrize("asobject", [True, False]) def test_interval(self, asobject): - idx = pd.IntervalIndex.from_breaks(range(5), closed="both") + idx = pd.IntervalIndex.from_breaks(range(5), inclusive="both") if asobject: idx = idx.astype(object) @@ -1668,21 +1668,21 @@ def test_interval(self, asobject): @pytest.mark.parametrize("value", [Timestamp(0), Timedelta(0), 0, 0.0]) def test_interval_mismatched_closed(self, value): - first = Interval(value, value, closed="left") - second = Interval(value, value, closed="right") + first = Interval(value, value, inclusive="left") + second = Interval(value, value, inclusive="right") - # if closed match, we should infer "interval" + # if inclusive match, we should infer "interval" arr = np.array([first, first], dtype=object) assert lib.infer_dtype(arr, skipna=False) == "interval" - # if closed dont match, we should _not_ get "interval" + # if inclusive dont match, we should _not_ get "interval" arr2 = np.array([first, second], dtype=object) assert lib.infer_dtype(arr2, skipna=False) == "mixed" def test_interval_mismatched_subtype(self): - first = Interval(0, 1, closed="left") - second = Interval(Timestamp(0), Timestamp(1), closed="left") - third = Interval(Timedelta(0), Timedelta(1), closed="left") + first = Interval(0, 1, inclusive="left") + second = Interval(Timestamp(0), Timestamp(1), inclusive="left") + third = Interval(Timedelta(0), Timedelta(1), inclusive="left") arr = np.array([first, second]) assert lib.infer_dtype(arr, skipna=False) == "mixed" @@ -1694,7 +1694,7 @@ def test_interval_mismatched_subtype(self): assert lib.infer_dtype(arr, skipna=False) == "mixed" # float vs int subdtype are compatible - flt_interval = Interval(1.5, 2.5, closed="left") + flt_interval = Interval(1.5, 2.5, inclusive="left") arr = np.array([first, flt_interval], dtype=object) assert lib.infer_dtype(arr, skipna=False) == "interval" diff --git a/pandas/tests/extension/base/setitem.py b/pandas/tests/extension/base/setitem.py index 283b67bb9171d..9e016e0101ef6 100644 --- a/pandas/tests/extension/base/setitem.py +++ b/pandas/tests/extension/base/setitem.py @@ -10,6 +10,7 @@ import pandas as pd import pandas._testing as tm +from pandas.core.arrays import IntervalArray from pandas.tests.extension.base.base import BaseExtensionTests @@ -76,10 +77,17 @@ def test_setitem_sequence_mismatched_length_raises(self, data, as_array): self.assert_series_equal(ser, original) def test_setitem_empty_indexer(self, data, box_in_series): + data_dtype = type(data) + if box_in_series: data = pd.Series(data) original = data.copy() - data[np.array([], dtype=int)] = [] + + if data_dtype == IntervalArray: + data[np.array([], dtype=int)] = IntervalArray([], "right") + else: + data[np.array([], dtype=int)] = [] + self.assert_equal(data, original) def test_setitem_sequence_broadcasts(self, data, box_in_series): diff --git a/pandas/tests/extension/test_interval.py b/pandas/tests/extension/test_interval.py index 0f916cea9d518..eb307d964d736 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -30,7 +30,9 @@ def make_data(): N = 100 left_array = np.random.uniform(size=N).cumsum() right_array = left_array + np.random.uniform(size=N) - return [Interval(left, right) for left, right in zip(left_array, right_array)] + return [ + Interval(left, right, "right") for left, right in zip(left_array, right_array) + ] @pytest.fixture @@ -41,7 +43,7 @@ def dtype(): @pytest.fixture def data(): """Length-100 PeriodArray for semantics test.""" - return IntervalArray(make_data()) + return IntervalArray(make_data(), "right") @pytest.fixture diff --git a/pandas/tests/frame/constructors/test_from_records.py b/pandas/tests/frame/constructors/test_from_records.py index c6d54e28ca1c8..715f69cc03828 100644 --- a/pandas/tests/frame/constructors/test_from_records.py +++ b/pandas/tests/frame/constructors/test_from_records.py @@ -229,7 +229,11 @@ def test_from_records_series_list_dict(self): def test_from_records_series_categorical_index(self): # GH#32805 index = CategoricalIndex( - [Interval(-20, -10), Interval(-10, 0), Interval(0, 10)] + [ + Interval(-20, -10, "right"), + Interval(-10, 0, "right"), + Interval(0, 10, "right"), + ] ) series_of_dicts = Series([{"a": 1}, {"a": 2}, {"b": 3}], index=index) frame = DataFrame.from_records(series_of_dicts, index=index) diff --git a/pandas/tests/frame/indexing/test_setitem.py b/pandas/tests/frame/indexing/test_setitem.py index fda37fdedb92a..04d95953a3a8d 100644 --- a/pandas/tests/frame/indexing/test_setitem.py +++ b/pandas/tests/frame/indexing/test_setitem.py @@ -227,7 +227,10 @@ def test_setitem_dict_preserves_dtypes(self): "obj,dtype", [ (Period("2020-01"), PeriodDtype("M")), - (Interval(left=0, right=5), IntervalDtype("int64", "right")), + ( + Interval(left=0, right=5, inclusive="right"), + IntervalDtype("int64", "right"), + ), ( Timestamp("2011-01-01", tz="US/Eastern"), DatetimeTZDtype(tz="US/Eastern"), diff --git a/pandas/tests/frame/methods/test_combine_first.py b/pandas/tests/frame/methods/test_combine_first.py index 47ebca0b9bf5c..783bef3206d58 100644 --- a/pandas/tests/frame/methods/test_combine_first.py +++ b/pandas/tests/frame/methods/test_combine_first.py @@ -402,7 +402,7 @@ def test_combine_first_string_dtype_only_na(self, nullable_string_dtype): (datetime(2020, 1, 1), datetime(2020, 1, 2)), (pd.Period("2020-01-01", "D"), pd.Period("2020-01-02", "D")), (pd.Timedelta("89 days"), pd.Timedelta("60 min")), - (pd.Interval(left=0, right=1), pd.Interval(left=2, right=3, closed="left")), + (pd.Interval(left=0, right=1), pd.Interval(left=2, right=3, inclusive="left")), ], ) def test_combine_first_timestamp_bug(scalar1, scalar2, nulls_fixture): diff --git a/pandas/tests/frame/methods/test_reset_index.py b/pandas/tests/frame/methods/test_reset_index.py index 37431bc291b76..bd168e4f14558 100644 --- a/pandas/tests/frame/methods/test_reset_index.py +++ b/pandas/tests/frame/methods/test_reset_index.py @@ -751,7 +751,7 @@ def test_reset_index_interval_columns_object_cast(): result = df.reset_index() expected = DataFrame( [[1, 1.0, 0.0], [2, 0.0, 1.0]], - columns=Index(["Year", Interval(0, 1), Interval(1, 2)]), + columns=Index(["Year", Interval(0, 1, "right"), Interval(1, 2, "right")]), ) tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/frame/methods/test_sort_index.py b/pandas/tests/frame/methods/test_sort_index.py index 5d1cc3d4ecee5..9cad965e9cb5c 100644 --- a/pandas/tests/frame/methods/test_sort_index.py +++ b/pandas/tests/frame/methods/test_sort_index.py @@ -384,7 +384,7 @@ def test_sort_index_intervalindex(self): result = model.groupby(["X1", "X2"], observed=True).mean().unstack() expected = IntervalIndex.from_tuples( - [(-3.0, -0.5), (-0.5, 0.0), (0.0, 0.5), (0.5, 3.0)], closed="right" + [(-3.0, -0.5), (-0.5, 0.0), (0.0, 0.5), (0.5, 3.0)], inclusive="right" ) result = result.columns.levels[1].categories tm.assert_index_equal(result, expected) @@ -729,7 +729,11 @@ def test_sort_index_multilevel_repr_8017(self, gen, extra): [ pytest.param(["a", "b", "c"], id="str"), pytest.param( - [pd.Interval(0, 1), pd.Interval(1, 2), pd.Interval(2, 3)], + [ + pd.Interval(0, 1, "right"), + pd.Interval(1, 2, "right"), + pd.Interval(2, 3, "right"), + ], id="pd.Interval", ), ], diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 82c7117cc00c6..e62c050fbf812 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -884,7 +884,10 @@ def test_constructor_dict_extension_scalar(self, ea_scalar_and_dtype): "data,dtype", [ (Period("2020-01"), PeriodDtype("M")), - (Interval(left=0, right=5), IntervalDtype("int64", "right")), + ( + Interval(left=0, right=5, inclusive="right"), + IntervalDtype("int64", "right"), + ), ( Timestamp("2011-01-01", tz="US/Eastern"), DatetimeTZDtype(tz="US/Eastern"), @@ -2410,16 +2413,16 @@ def test_constructor_series_nonexact_categoricalindex(self): result = DataFrame({"1": ser1, "2": ser2}) index = CategoricalIndex( [ - Interval(-0.099, 9.9, closed="right"), - Interval(9.9, 19.8, closed="right"), - Interval(19.8, 29.7, closed="right"), - Interval(29.7, 39.6, closed="right"), - Interval(39.6, 49.5, closed="right"), - Interval(49.5, 59.4, closed="right"), - Interval(59.4, 69.3, closed="right"), - Interval(69.3, 79.2, closed="right"), - Interval(79.2, 89.1, closed="right"), - Interval(89.1, 99, closed="right"), + Interval(-0.099, 9.9, inclusive="right"), + Interval(9.9, 19.8, inclusive="right"), + Interval(19.8, 29.7, inclusive="right"), + Interval(29.7, 39.6, inclusive="right"), + Interval(39.6, 49.5, inclusive="right"), + Interval(49.5, 59.4, inclusive="right"), + Interval(59.4, 69.3, inclusive="right"), + Interval(69.3, 79.2, inclusive="right"), + Interval(79.2, 89.1, inclusive="right"), + Interval(89.1, 99, inclusive="right"), ], ordered=True, ) diff --git a/pandas/tests/groupby/test_grouping.py b/pandas/tests/groupby/test_grouping.py index 5f1a81c504efe..2c2332a05505f 100644 --- a/pandas/tests/groupby/test_grouping.py +++ b/pandas/tests/groupby/test_grouping.py @@ -799,13 +799,13 @@ def test_get_group_empty_bins(self, observed): # TODO: should prob allow a str of Interval work as well # IOW '(0, 5]' - result = g.get_group(pd.Interval(0, 5)) + result = g.get_group(pd.Interval(0, 5, "right")) expected = DataFrame([3, 1], index=[0, 1]) tm.assert_frame_equal(result, expected) - msg = r"Interval\(10, 15, closed='right'\)" + msg = r"Interval\(10, 15, inclusive='right'\)" with pytest.raises(KeyError, match=msg): - g.get_group(pd.Interval(10, 15)) + g.get_group(pd.Interval(10, 15, "right")) def test_get_group_grouped_by_tuple(self): # GH 8121 diff --git a/pandas/tests/indexes/categorical/test_astype.py b/pandas/tests/indexes/categorical/test_astype.py index 854ae8b62db30..ec3e3dca92808 100644 --- a/pandas/tests/indexes/categorical/test_astype.py +++ b/pandas/tests/indexes/categorical/test_astype.py @@ -26,7 +26,9 @@ def test_astype(self): assert not isinstance(result, CategoricalIndex) # interval - ii = IntervalIndex.from_arrays(left=[-0.001, 2.0], right=[2, 4], closed="right") + ii = IntervalIndex.from_arrays( + left=[-0.001, 2.0], right=[2, 4], inclusive="right" + ) ci = CategoricalIndex( Categorical.from_codes([0, 1, -1], categories=ii, ordered=True) diff --git a/pandas/tests/indexes/categorical/test_reindex.py b/pandas/tests/indexes/categorical/test_reindex.py index 1337eff1f1c2f..8764063a1a008 100644 --- a/pandas/tests/indexes/categorical/test_reindex.py +++ b/pandas/tests/indexes/categorical/test_reindex.py @@ -69,15 +69,15 @@ def test_reindex_empty_index(self): def test_reindex_categorical_added_category(self): # GH 42424 ci = CategoricalIndex( - [Interval(0, 1, closed="right"), Interval(1, 2, closed="right")], + [Interval(0, 1, inclusive="right"), Interval(1, 2, inclusive="right")], ordered=True, ) ci_add = CategoricalIndex( [ - Interval(0, 1, closed="right"), - Interval(1, 2, closed="right"), - Interval(2, 3, closed="right"), - Interval(3, 4, closed="right"), + Interval(0, 1, inclusive="right"), + Interval(1, 2, inclusive="right"), + Interval(2, 3, inclusive="right"), + Interval(3, 4, inclusive="right"), ], ordered=True, ) diff --git a/pandas/tests/indexes/interval/test_astype.py b/pandas/tests/indexes/interval/test_astype.py index 4cdbe2bbcf12b..6751a383699bb 100644 --- a/pandas/tests/indexes/interval/test_astype.py +++ b/pandas/tests/indexes/interval/test_astype.py @@ -82,7 +82,7 @@ class TestIntSubtype(AstypeTests): indexes = [ IntervalIndex.from_breaks(np.arange(-10, 11, dtype="int64")), - IntervalIndex.from_breaks(np.arange(100, dtype="uint64"), closed="left"), + IntervalIndex.from_breaks(np.arange(100, dtype="uint64"), inclusive="left"), ] @pytest.fixture(params=indexes) @@ -93,10 +93,12 @@ def index(self, request): "subtype", ["float64", "datetime64[ns]", "timedelta64[ns]"] ) def test_subtype_conversion(self, index, subtype): - dtype = IntervalDtype(subtype, index.closed) + dtype = IntervalDtype(subtype, index.inclusive) result = index.astype(dtype) expected = IntervalIndex.from_arrays( - index.left.astype(subtype), index.right.astype(subtype), closed=index.closed + index.left.astype(subtype), + index.right.astype(subtype), + inclusive=index.inclusive, ) tm.assert_index_equal(result, expected) @@ -105,12 +107,12 @@ def test_subtype_conversion(self, index, subtype): ) def test_subtype_integer(self, subtype_start, subtype_end): index = IntervalIndex.from_breaks(np.arange(100, dtype=subtype_start)) - dtype = IntervalDtype(subtype_end, index.closed) + dtype = IntervalDtype(subtype_end, index.inclusive) result = index.astype(dtype) expected = IntervalIndex.from_arrays( index.left.astype(subtype_end), index.right.astype(subtype_end), - closed=index.closed, + inclusive=index.inclusive, ) tm.assert_index_equal(result, expected) @@ -135,7 +137,9 @@ class TestFloatSubtype(AstypeTests): indexes = [ interval_range(-10.0, 10.0, inclusive="neither"), IntervalIndex.from_arrays( - [-1.5, np.nan, 0.0, 0.0, 1.5], [-0.5, np.nan, 1.0, 1.0, 3.0], closed="both" + [-1.5, np.nan, 0.0, 0.0, 1.5], + [-0.5, np.nan, 1.0, 1.0, 3.0], + inclusive="both", ), ] @@ -149,7 +153,9 @@ def test_subtype_integer(self, subtype): dtype = IntervalDtype(subtype, "right") result = index.astype(dtype) expected = IntervalIndex.from_arrays( - index.left.astype(subtype), index.right.astype(subtype), closed=index.closed + index.left.astype(subtype), + index.right.astype(subtype), + inclusive=index.inclusive, ) tm.assert_index_equal(result, expected) @@ -164,7 +170,9 @@ def test_subtype_integer_with_non_integer_borders(self, subtype): dtype = IntervalDtype(subtype, "right") result = index.astype(dtype) expected = IntervalIndex.from_arrays( - index.left.astype(subtype), index.right.astype(subtype), closed=index.closed + index.left.astype(subtype), + index.right.astype(subtype), + inclusive=index.inclusive, ) tm.assert_index_equal(result, expected) @@ -216,7 +224,9 @@ def test_subtype_integer(self, index, subtype): new_left = index.left.astype(subtype) new_right = index.right.astype(subtype) - expected = IntervalIndex.from_arrays(new_left, new_right, closed=index.closed) + expected = IntervalIndex.from_arrays( + new_left, new_right, inclusive=index.inclusive + ) tm.assert_index_equal(result, expected) def test_subtype_float(self, index): diff --git a/pandas/tests/indexes/interval/test_base.py b/pandas/tests/indexes/interval/test_base.py index c44303aa2c862..933707bfe8357 100644 --- a/pandas/tests/indexes/interval/test_base.py +++ b/pandas/tests/indexes/interval/test_base.py @@ -16,14 +16,14 @@ class TestBase(Base): @pytest.fixture def simple_index(self) -> IntervalIndex: - return self._index_cls.from_breaks(range(11), closed="right") + return self._index_cls.from_breaks(range(11), inclusive="right") @pytest.fixture def index(self): return tm.makeIntervalIndex(10) - def create_index(self, *, closed="right"): - return IntervalIndex.from_breaks(range(11), closed=closed) + def create_index(self, *, inclusive="right"): + return IntervalIndex.from_breaks(range(11), inclusive=inclusive) def test_repr_max_seq_item_setting(self): # override base test: not a valid repr as we use interval notation @@ -34,13 +34,13 @@ def test_repr_roundtrip(self): pass def test_take(self, closed): - index = self.create_index(closed=closed) + index = self.create_index(inclusive=closed) result = index.take(range(10)) tm.assert_index_equal(result, index) result = index.take([0, 0, 1]) - expected = IntervalIndex.from_arrays([0, 0, 1], [1, 1, 2], closed=closed) + expected = IntervalIndex.from_arrays([0, 0, 1], [1, 1, 2], inclusive=closed) tm.assert_index_equal(result, expected) def test_where(self, simple_index, listlike_box): diff --git a/pandas/tests/indexes/interval/test_constructors.py b/pandas/tests/indexes/interval/test_constructors.py index a71a8f9e34ea9..b57bcf7abc1e1 100644 --- a/pandas/tests/indexes/interval/test_constructors.py +++ b/pandas/tests/indexes/interval/test_constructors.py @@ -53,9 +53,9 @@ class ConstructorTests: ) def test_constructor(self, constructor, breaks, closed, name): result_kwargs = self.get_kwargs_from_breaks(breaks, closed) - result = constructor(closed=closed, name=name, **result_kwargs) + result = constructor(inclusive=closed, name=name, **result_kwargs) - assert result.closed == closed + assert result.inclusive == closed assert result.name == name assert result.dtype.subtype == getattr(breaks, "dtype", "int64") tm.assert_index_equal(result.left, Index(breaks[:-1])) @@ -78,7 +78,7 @@ def test_constructor_dtype(self, constructor, breaks, subtype): expected = constructor(**expected_kwargs) result_kwargs = self.get_kwargs_from_breaks(breaks) - iv_dtype = IntervalDtype(subtype, "right") + iv_dtype = IntervalDtype(subtype, "both") for dtype in (iv_dtype, str(iv_dtype)): result = constructor(dtype=dtype, **result_kwargs) tm.assert_index_equal(result, expected) @@ -108,20 +108,20 @@ def test_constructor_pass_closed(self, constructor, breaks): for dtype in (iv_dtype, str(iv_dtype)): with tm.assert_produces_warning(warn): - result = constructor(dtype=dtype, closed="left", **result_kwargs) - assert result.dtype.closed == "left" + result = constructor(dtype=dtype, inclusive="left", **result_kwargs) + assert result.dtype.inclusive == "left" @pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning") @pytest.mark.parametrize("breaks", [[np.nan] * 2, [np.nan] * 4, [np.nan] * 50]) def test_constructor_nan(self, constructor, breaks, closed): # GH 18421 result_kwargs = self.get_kwargs_from_breaks(breaks) - result = constructor(closed=closed, **result_kwargs) + result = constructor(inclusive=closed, **result_kwargs) expected_subtype = np.float64 expected_values = np.array(breaks[:-1], dtype=object) - assert result.closed == closed + assert result.inclusive == closed assert result.dtype.subtype == expected_subtype tm.assert_numpy_array_equal(np.array(result), expected_values) @@ -139,13 +139,13 @@ def test_constructor_nan(self, constructor, breaks, closed): def test_constructor_empty(self, constructor, breaks, closed): # GH 18421 result_kwargs = self.get_kwargs_from_breaks(breaks) - result = constructor(closed=closed, **result_kwargs) + result = constructor(inclusive=closed, **result_kwargs) expected_values = np.array([], dtype=object) expected_subtype = getattr(breaks, "dtype", np.int64) assert result.empty - assert result.closed == closed + assert result.inclusive == closed assert result.dtype.subtype == expected_subtype tm.assert_numpy_array_equal(np.array(result), expected_values) @@ -184,9 +184,9 @@ def test_generic_errors(self, constructor): filler = self.get_kwargs_from_breaks(range(10)) # invalid closed - msg = "closed must be one of 'right', 'left', 'both', 'neither'" + msg = "inclusive must be one of 'right', 'left', 'both', 'neither'" with pytest.raises(ValueError, match=msg): - constructor(closed="invalid", **filler) + constructor(inclusive="invalid", **filler) # unsupported dtype msg = "dtype must be an IntervalDtype, got int64" @@ -219,7 +219,7 @@ class TestFromArrays(ConstructorTests): def constructor(self): return IntervalIndex.from_arrays - def get_kwargs_from_breaks(self, breaks, closed="right"): + def get_kwargs_from_breaks(self, breaks, inclusive="both"): """ converts intervals in breaks format to a dictionary of kwargs to specific to the format expected by IntervalIndex.from_arrays @@ -268,7 +268,7 @@ class TestFromBreaks(ConstructorTests): def constructor(self): return IntervalIndex.from_breaks - def get_kwargs_from_breaks(self, breaks, closed="right"): + def get_kwargs_from_breaks(self, breaks, inclusive="both"): """ converts intervals in breaks format to a dictionary of kwargs to specific to the format expected by IntervalIndex.from_breaks @@ -306,7 +306,7 @@ class TestFromTuples(ConstructorTests): def constructor(self): return IntervalIndex.from_tuples - def get_kwargs_from_breaks(self, breaks, closed="right"): + def get_kwargs_from_breaks(self, breaks, inclusive="both"): """ converts intervals in breaks format to a dictionary of kwargs to specific to the format expected by IntervalIndex.from_tuples @@ -356,7 +356,7 @@ class TestClassConstructors(ConstructorTests): def constructor(self, request): return request.param - def get_kwargs_from_breaks(self, breaks, closed="right"): + def get_kwargs_from_breaks(self, breaks, inclusive="both"): """ converts intervals in breaks format to a dictionary of kwargs to specific to the format expected by the IntervalIndex/Index constructors @@ -365,7 +365,7 @@ def get_kwargs_from_breaks(self, breaks, closed="right"): return {"data": breaks} ivs = [ - Interval(left, right, closed) if notna(left) else left + Interval(left, right, inclusive) if notna(left) else left for left, right in zip(breaks[:-1], breaks[1:]) ] @@ -390,7 +390,7 @@ def test_constructor_string(self): def test_constructor_errors(self, constructor): # mismatched closed within intervals with no constructor override - ivs = [Interval(0, 1, closed="right"), Interval(2, 3, closed="left")] + ivs = [Interval(0, 1, inclusive="right"), Interval(2, 3, inclusive="left")] msg = "intervals must all be closed on the same side" with pytest.raises(ValueError, match=msg): constructor(ivs) @@ -415,14 +415,17 @@ def test_constructor_errors(self, constructor): ([], "both"), ([np.nan, np.nan], "neither"), ( - [Interval(0, 3, closed="neither"), Interval(2, 5, closed="neither")], + [ + Interval(0, 3, inclusive="neither"), + Interval(2, 5, inclusive="neither"), + ], "left", ), ( - [Interval(0, 3, closed="left"), Interval(2, 5, closed="right")], + [Interval(0, 3, inclusive="left"), Interval(2, 5, inclusive="right")], "neither", ), - (IntervalIndex.from_breaks(range(5), closed="both"), "right"), + (IntervalIndex.from_breaks(range(5), inclusive="both"), "right"), ], ) def test_override_inferred_closed(self, constructor, data, closed): @@ -431,8 +434,8 @@ def test_override_inferred_closed(self, constructor, data, closed): tuples = data.to_tuples() else: tuples = [(iv.left, iv.right) if notna(iv) else iv for iv in data] - expected = IntervalIndex.from_tuples(tuples, closed=closed) - result = constructor(data, closed=closed) + expected = IntervalIndex.from_tuples(tuples, inclusive=closed) + result = constructor(data, inclusive=closed) tm.assert_index_equal(result, expected) @pytest.mark.parametrize( @@ -450,10 +453,10 @@ def test_index_object_dtype(self, values_constructor): def test_index_mixed_closed(self): # GH27172 intervals = [ - Interval(0, 1, closed="left"), - Interval(1, 2, closed="right"), - Interval(2, 3, closed="neither"), - Interval(3, 4, closed="both"), + Interval(0, 1, inclusive="left"), + Interval(1, 2, inclusive="right"), + Interval(2, 3, inclusive="neither"), + Interval(3, 4, inclusive="both"), ] result = Index(intervals) expected = Index(intervals, dtype=object) @@ -465,9 +468,9 @@ def test_dtype_closed_mismatch(): dtype = IntervalDtype(np.int64, "left") - msg = "closed keyword does not match dtype.closed" + msg = "inclusive keyword does not match dtype.inclusive" with pytest.raises(ValueError, match=msg): - IntervalIndex([], dtype=dtype, closed="neither") + IntervalIndex([], dtype=dtype, inclusive="neither") with pytest.raises(ValueError, match=msg): - IntervalArray([], dtype=dtype, closed="neither") + IntervalArray([], dtype=dtype, inclusive="neither") diff --git a/pandas/tests/indexes/interval/test_equals.py b/pandas/tests/indexes/interval/test_equals.py index 87e2348e5fdb3..a873116600d6d 100644 --- a/pandas/tests/indexes/interval/test_equals.py +++ b/pandas/tests/indexes/interval/test_equals.py @@ -8,7 +8,7 @@ class TestEquals: def test_equals(self, closed): - expected = IntervalIndex.from_breaks(np.arange(5), closed=closed) + expected = IntervalIndex.from_breaks(np.arange(5), inclusive=closed) assert expected.equals(expected) assert expected.equals(expected.copy()) @@ -21,16 +21,16 @@ def test_equals(self, closed): assert not expected.equals(date_range("20130101", periods=2)) expected_name1 = IntervalIndex.from_breaks( - np.arange(5), closed=closed, name="foo" + np.arange(5), inclusive=closed, name="foo" ) expected_name2 = IntervalIndex.from_breaks( - np.arange(5), closed=closed, name="bar" + np.arange(5), inclusive=closed, name="bar" ) assert expected.equals(expected_name1) assert expected_name1.equals(expected_name2) - for other_closed in {"left", "right", "both", "neither"} - {closed}: - expected_other_closed = IntervalIndex.from_breaks( - np.arange(5), closed=other_closed + for other_inclusive in {"left", "right", "both", "neither"} - {closed}: + expected_other_inclusive = IntervalIndex.from_breaks( + np.arange(5), inclusive=other_inclusive ) - assert not expected.equals(expected_other_closed) + assert not expected.equals(expected_other_inclusive) diff --git a/pandas/tests/indexes/interval/test_formats.py b/pandas/tests/indexes/interval/test_formats.py index db477003900bc..2d9b8c83c7ab2 100644 --- a/pandas/tests/indexes/interval/test_formats.py +++ b/pandas/tests/indexes/interval/test_formats.py @@ -17,7 +17,8 @@ class TestIntervalIndexRendering: def test_frame_repr(self): # https://github.com/pandas-dev/pandas/pull/24134/files df = DataFrame( - {"A": [1, 2, 3, 4]}, index=IntervalIndex.from_breaks([0, 1, 2, 3, 4]) + {"A": [1, 2, 3, 4]}, + index=IntervalIndex.from_breaks([0, 1, 2, 3, 4], "right"), ) result = repr(df) expected = " A\n(0, 1] 1\n(1, 2] 2\n(2, 3] 3\n(3, 4] 4" @@ -40,7 +41,7 @@ def test_frame_repr(self): ) def test_repr_missing(self, constructor, expected): # GH 25984 - index = IntervalIndex.from_tuples([(0, 1), np.nan, (2, 3)]) + index = IntervalIndex.from_tuples([(0, 1), np.nan, (2, 3)], "right") obj = constructor(list("abc"), index=index) result = repr(obj) assert result == expected @@ -57,7 +58,8 @@ def test_repr_floats(self): Float64Index([329.973, 345.137], dtype="float64"), Float64Index([345.137, 360.191], dtype="float64"), ) - ] + ], + "right", ), ) result = str(markers) @@ -65,7 +67,7 @@ def test_repr_floats(self): assert result == expected @pytest.mark.parametrize( - "tuples, closed, expected_data", + "tuples, inclusive, expected_data", [ ([(0, 1), (1, 2), (2, 3)], "left", ["[0, 1)", "[1, 2)", "[2, 3)"]), ( @@ -97,9 +99,9 @@ def test_repr_floats(self): ), ], ) - def test_to_native_types(self, tuples, closed, expected_data): + def test_to_native_types(self, tuples, inclusive, expected_data): # GH 28210 - index = IntervalIndex.from_tuples(tuples, closed=closed) + index = IntervalIndex.from_tuples(tuples, inclusive=inclusive) result = index._format_native_types() expected = np.array(expected_data) tm.assert_numpy_array_equal(result, expected) diff --git a/pandas/tests/indexes/interval/test_indexing.py b/pandas/tests/indexes/interval/test_indexing.py index 7c00b23dc9ac4..4cf754a7e52e0 100644 --- a/pandas/tests/indexes/interval/test_indexing.py +++ b/pandas/tests/indexes/interval/test_indexing.py @@ -25,23 +25,23 @@ class TestGetLoc: @pytest.mark.parametrize("side", ["right", "left", "both", "neither"]) def test_get_loc_interval(self, closed, side): - idx = IntervalIndex.from_tuples([(0, 1), (2, 3)], closed=closed) + idx = IntervalIndex.from_tuples([(0, 1), (2, 3)], inclusive=closed) for bound in [[0, 1], [1, 2], [2, 3], [3, 4], [0, 2], [2.5, 3], [-1, 4]]: # if get_loc is supplied an interval, it should only search # for exact matches, not overlaps or covers, else KeyError. - msg = re.escape(f"Interval({bound[0]}, {bound[1]}, closed='{side}')") + msg = re.escape(f"Interval({bound[0]}, {bound[1]}, inclusive='{side}')") if closed == side: if bound == [0, 1]: - assert idx.get_loc(Interval(0, 1, closed=side)) == 0 + assert idx.get_loc(Interval(0, 1, inclusive=side)) == 0 elif bound == [2, 3]: - assert idx.get_loc(Interval(2, 3, closed=side)) == 1 + assert idx.get_loc(Interval(2, 3, inclusive=side)) == 1 else: with pytest.raises(KeyError, match=msg): - idx.get_loc(Interval(*bound, closed=side)) + idx.get_loc(Interval(*bound, inclusive=side)) else: with pytest.raises(KeyError, match=msg): - idx.get_loc(Interval(*bound, closed=side)) + idx.get_loc(Interval(*bound, inclusive=side)) @pytest.mark.parametrize("scalar", [-0.5, 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5]) def test_get_loc_scalar(self, closed, scalar): @@ -55,7 +55,7 @@ def test_get_loc_scalar(self, closed, scalar): "neither": {0.5: 0, 2.5: 1}, } - idx = IntervalIndex.from_tuples([(0, 1), (2, 3)], closed=closed) + idx = IntervalIndex.from_tuples([(0, 1), (2, 3)], inclusive=closed) # if get_loc is supplied a scalar, it should return the index of # the interval which contains the scalar, or KeyError. @@ -68,7 +68,7 @@ def test_get_loc_scalar(self, closed, scalar): @pytest.mark.parametrize("scalar", [-1, 0, 0.5, 3, 4.5, 5, 6]) def test_get_loc_length_one_scalar(self, scalar, closed): # GH 20921 - index = IntervalIndex.from_tuples([(0, 5)], closed=closed) + index = IntervalIndex.from_tuples([(0, 5)], inclusive=closed) if scalar in index[0]: result = index.get_loc(scalar) assert result == 0 @@ -80,15 +80,17 @@ def test_get_loc_length_one_scalar(self, scalar, closed): @pytest.mark.parametrize("left, right", [(0, 5), (-1, 4), (-1, 6), (6, 7)]) def test_get_loc_length_one_interval(self, left, right, closed, other_closed): # GH 20921 - index = IntervalIndex.from_tuples([(0, 5)], closed=closed) - interval = Interval(left, right, closed=other_closed) + index = IntervalIndex.from_tuples([(0, 5)], inclusive=closed) + interval = Interval(left, right, inclusive=other_closed) if interval == index[0]: result = index.get_loc(interval) assert result == 0 else: with pytest.raises( KeyError, - match=re.escape(f"Interval({left}, {right}, closed='{other_closed}')"), + match=re.escape( + f"Interval({left}, {right}, inclusive='{other_closed}')" + ), ): index.get_loc(interval) @@ -192,23 +194,35 @@ class TestGetIndexer: @pytest.mark.parametrize( "query, expected", [ - ([Interval(2, 4, closed="right")], [1]), - ([Interval(2, 4, closed="left")], [-1]), - ([Interval(2, 4, closed="both")], [-1]), - ([Interval(2, 4, closed="neither")], [-1]), - ([Interval(1, 4, closed="right")], [-1]), - ([Interval(0, 4, closed="right")], [-1]), - ([Interval(0.5, 1.5, closed="right")], [-1]), - ([Interval(2, 4, closed="right"), Interval(0, 1, closed="right")], [1, -1]), - ([Interval(2, 4, closed="right"), Interval(2, 4, closed="right")], [1, 1]), - ([Interval(5, 7, closed="right"), Interval(2, 4, closed="right")], [2, 1]), - ([Interval(2, 4, closed="right"), Interval(2, 4, closed="left")], [1, -1]), + ([Interval(2, 4, inclusive="right")], [1]), + ([Interval(2, 4, inclusive="left")], [-1]), + ([Interval(2, 4, inclusive="both")], [-1]), + ([Interval(2, 4, inclusive="neither")], [-1]), + ([Interval(1, 4, inclusive="right")], [-1]), + ([Interval(0, 4, inclusive="right")], [-1]), + ([Interval(0.5, 1.5, inclusive="right")], [-1]), + ( + [Interval(2, 4, inclusive="right"), Interval(0, 1, inclusive="right")], + [1, -1], + ), + ( + [Interval(2, 4, inclusive="right"), Interval(2, 4, inclusive="right")], + [1, 1], + ), + ( + [Interval(5, 7, inclusive="right"), Interval(2, 4, inclusive="right")], + [2, 1], + ), + ( + [Interval(2, 4, inclusive="right"), Interval(2, 4, inclusive="left")], + [1, -1], + ), ], ) def test_get_indexer_with_interval(self, query, expected): tuples = [(0, 2), (2, 4), (5, 7)] - index = IntervalIndex.from_tuples(tuples, closed="right") + index = IntervalIndex.from_tuples(tuples, inclusive="right") result = index.get_indexer(query) expected = np.array(expected, dtype="intp") @@ -237,7 +251,7 @@ def test_get_indexer_with_interval(self, query, expected): def test_get_indexer_with_int_and_float(self, query, expected): tuples = [(0, 1), (1, 2), (3, 4)] - index = IntervalIndex.from_tuples(tuples, closed="right") + index = IntervalIndex.from_tuples(tuples, inclusive="right") result = index.get_indexer(query) expected = np.array(expected, dtype="intp") @@ -246,7 +260,7 @@ def test_get_indexer_with_int_and_float(self, query, expected): @pytest.mark.parametrize("item", [[3], np.arange(0.5, 5, 0.5)]) def test_get_indexer_length_one(self, item, closed): # GH 17284 - index = IntervalIndex.from_tuples([(0, 5)], closed=closed) + index = IntervalIndex.from_tuples([(0, 5)], inclusive=closed) result = index.get_indexer(item) expected = np.array([0] * len(item), dtype="intp") tm.assert_numpy_array_equal(result, expected) @@ -254,7 +268,7 @@ def test_get_indexer_length_one(self, item, closed): @pytest.mark.parametrize("size", [1, 5]) def test_get_indexer_length_one_interval(self, size, closed): # GH 17284 - index = IntervalIndex.from_tuples([(0, 5)], closed=closed) + index = IntervalIndex.from_tuples([(0, 5)], inclusive=closed) result = index.get_indexer([Interval(0, 5, closed)] * size) expected = np.array([0] * size, dtype="intp") tm.assert_numpy_array_equal(result, expected) @@ -264,14 +278,14 @@ def test_get_indexer_length_one_interval(self, size, closed): [ IntervalIndex.from_tuples([(7, 8), (1, 2), (3, 4), (0, 1)]), IntervalIndex.from_tuples([(0, 1), (1, 2), (3, 4), np.nan]), - IntervalIndex.from_tuples([(0, 1), (1, 2), (3, 4)], closed="both"), + IntervalIndex.from_tuples([(0, 1), (1, 2), (3, 4)], inclusive="both"), [-1, 0, 0.5, 1, 2, 2.5, np.nan], ["foo", "foo", "bar", "baz"], ], ) def test_get_indexer_categorical(self, target, ordered): # GH 30063: categorical and non-categorical results should be consistent - index = IntervalIndex.from_tuples([(0, 1), (1, 2), (3, 4)]) + index = IntervalIndex.from_tuples([(0, 1), (1, 2), (3, 4)], inclusive="right") categorical_target = CategoricalIndex(target, ordered=ordered) result = index.get_indexer(categorical_target) @@ -280,7 +294,7 @@ def test_get_indexer_categorical(self, target, ordered): def test_get_indexer_categorical_with_nans(self): # GH#41934 nans in both index and in target - ii = IntervalIndex.from_breaks(range(5)) + ii = IntervalIndex.from_breaks(range(5), inclusive="right") ii2 = ii.append(IntervalIndex([np.nan])) ci2 = CategoricalIndex(ii2) @@ -299,7 +313,7 @@ def test_get_indexer_categorical_with_nans(self): tm.assert_numpy_array_equal(result, expected) @pytest.mark.parametrize( - "tuples, closed", + "tuples, inclusive", [ ([(0, 2), (1, 3), (3, 4)], "neither"), ([(0, 5), (1, 4), (6, 7)], "left"), @@ -307,9 +321,9 @@ def test_get_indexer_categorical_with_nans(self): ([(0, 1), (2, 3), (3, 4)], "both"), ], ) - def test_get_indexer_errors(self, tuples, closed): + def test_get_indexer_errors(self, tuples, inclusive): # IntervalIndex needs non-overlapping for uniqueness when querying - index = IntervalIndex.from_tuples(tuples, closed=closed) + index = IntervalIndex.from_tuples(tuples, inclusive=inclusive) msg = ( "cannot handle overlapping indices; use " @@ -341,7 +355,7 @@ def test_get_indexer_errors(self, tuples, closed): def test_get_indexer_non_unique_with_int_and_float(self, query, expected): tuples = [(0, 2.5), (1, 3), (2, 4)] - index = IntervalIndex.from_tuples(tuples, closed="left") + index = IntervalIndex.from_tuples(tuples, inclusive="left") result_indexer, result_missing = index.get_indexer_non_unique(query) expected_indexer = np.array(expected[0], dtype="intp") @@ -433,45 +447,45 @@ def test_slice_locs_with_interval(self): assert index.slice_locs(start=Interval(2, 4), end=Interval(0, 2)) == (2, 2) # unsorted duplicates - index = IntervalIndex.from_tuples([(0, 2), (2, 4), (0, 2)]) + index = IntervalIndex.from_tuples([(0, 2), (2, 4), (0, 2)], "right") with pytest.raises( KeyError, match=re.escape( '"Cannot get left slice bound for non-unique label: ' - "Interval(0, 2, closed='right')\"" + "Interval(0, 2, inclusive='right')\"" ), ): - index.slice_locs(start=Interval(0, 2), end=Interval(2, 4)) + index.slice_locs(start=Interval(0, 2, "right"), end=Interval(2, 4, "right")) with pytest.raises( KeyError, match=re.escape( '"Cannot get left slice bound for non-unique label: ' - "Interval(0, 2, closed='right')\"" + "Interval(0, 2, inclusive='right')\"" ), ): - index.slice_locs(start=Interval(0, 2)) + index.slice_locs(start=Interval(0, 2, "right")) - assert index.slice_locs(end=Interval(2, 4)) == (0, 2) + assert index.slice_locs(end=Interval(2, 4, "right")) == (0, 2) with pytest.raises( KeyError, match=re.escape( '"Cannot get right slice bound for non-unique label: ' - "Interval(0, 2, closed='right')\"" + "Interval(0, 2, inclusive='right')\"" ), ): - index.slice_locs(end=Interval(0, 2)) + index.slice_locs(end=Interval(0, 2, "right")) with pytest.raises( KeyError, match=re.escape( '"Cannot get right slice bound for non-unique label: ' - "Interval(0, 2, closed='right')\"" + "Interval(0, 2, inclusive='right')\"" ), ): - index.slice_locs(start=Interval(2, 4), end=Interval(0, 2)) + index.slice_locs(start=Interval(2, 4, "right"), end=Interval(0, 2, "right")) # another unsorted duplicates index = IntervalIndex.from_tuples([(0, 2), (0, 2), (2, 4), (1, 3)]) @@ -485,7 +499,7 @@ def test_slice_locs_with_interval(self): def test_slice_locs_with_ints_and_floats_succeeds(self): # increasing non-overlapping - index = IntervalIndex.from_tuples([(0, 1), (1, 2), (3, 4)]) + index = IntervalIndex.from_tuples([(0, 1), (1, 2), (3, 4)], inclusive="right") assert index.slice_locs(0, 1) == (0, 1) assert index.slice_locs(0, 2) == (0, 2) @@ -495,7 +509,7 @@ def test_slice_locs_with_ints_and_floats_succeeds(self): assert index.slice_locs(0, 4) == (0, 3) # decreasing non-overlapping - index = IntervalIndex.from_tuples([(3, 4), (1, 2), (0, 1)]) + index = IntervalIndex.from_tuples([(3, 4), (1, 2), (0, 1)], inclusive="right") assert index.slice_locs(0, 1) == (3, 3) assert index.slice_locs(0, 2) == (3, 2) assert index.slice_locs(0, 3) == (3, 1) @@ -516,7 +530,7 @@ def test_slice_locs_with_ints_and_floats_succeeds(self): ) def test_slice_locs_with_ints_and_floats_errors(self, tuples, query): start, stop = query - index = IntervalIndex.from_tuples(tuples) + index = IntervalIndex.from_tuples(tuples, inclusive="right") with pytest.raises( KeyError, match=( @@ -571,17 +585,17 @@ class TestContains: def test_contains_dunder(self): - index = IntervalIndex.from_arrays([0, 1], [1, 2], closed="right") + index = IntervalIndex.from_arrays([0, 1], [1, 2], inclusive="right") # __contains__ requires perfect matches to intervals. assert 0 not in index assert 1 not in index assert 2 not in index - assert Interval(0, 1, closed="right") in index - assert Interval(0, 2, closed="right") not in index - assert Interval(0, 0.5, closed="right") not in index - assert Interval(3, 5, closed="right") not in index - assert Interval(-1, 0, closed="left") not in index - assert Interval(0, 1, closed="left") not in index - assert Interval(0, 1, closed="both") not in index + assert Interval(0, 1, inclusive="right") in index + assert Interval(0, 2, inclusive="right") not in index + assert Interval(0, 0.5, inclusive="right") not in index + assert Interval(3, 5, inclusive="right") not in index + assert Interval(-1, 0, inclusive="left") not in index + assert Interval(0, 1, inclusive="left") not in index + assert Interval(0, 1, inclusive="both") not in index diff --git a/pandas/tests/indexes/interval/test_interval.py b/pandas/tests/indexes/interval/test_interval.py index 8880cab2ce29b..4e33c3abd3252 100644 --- a/pandas/tests/indexes/interval/test_interval.py +++ b/pandas/tests/indexes/interval/test_interval.py @@ -28,21 +28,21 @@ def name(request): class TestIntervalIndex: - index = IntervalIndex.from_arrays([0, 1], [1, 2]) + index = IntervalIndex.from_arrays([0, 1], [1, 2], "right") - def create_index(self, closed="right"): - return IntervalIndex.from_breaks(range(11), closed=closed) + def create_index(self, inclusive="right"): + return IntervalIndex.from_breaks(range(11), inclusive=inclusive) - def create_index_with_nan(self, closed="right"): + def create_index_with_nan(self, inclusive="right"): mask = [True, False] + [True] * 8 return IntervalIndex.from_arrays( np.where(mask, np.arange(10), np.nan), np.where(mask, np.arange(1, 11), np.nan), - closed=closed, + inclusive=inclusive, ) def test_properties(self, closed): - index = self.create_index(closed=closed) + index = self.create_index(inclusive=closed) assert len(index) == 10 assert index.size == 10 assert index.shape == (10,) @@ -51,7 +51,7 @@ def test_properties(self, closed): tm.assert_index_equal(index.right, Index(np.arange(1, 11))) tm.assert_index_equal(index.mid, Index(np.arange(0.5, 10.5))) - assert index.closed == closed + assert index.inclusive == closed ivs = [ Interval(left, right, closed) @@ -61,7 +61,7 @@ def test_properties(self, closed): tm.assert_numpy_array_equal(np.asarray(index), expected) # with nans - index = self.create_index_with_nan(closed=closed) + index = self.create_index_with_nan(inclusive=closed) assert len(index) == 10 assert index.size == 10 assert index.shape == (10,) @@ -73,7 +73,7 @@ def test_properties(self, closed): tm.assert_index_equal(index.right, expected_right) tm.assert_index_equal(index.mid, expected_mid) - assert index.closed == closed + assert index.inclusive == closed ivs = [ Interval(left, right, closed) if notna(left) else np.nan @@ -93,7 +93,7 @@ def test_properties(self, closed): ) def test_length(self, closed, breaks): # GH 18789 - index = IntervalIndex.from_breaks(breaks, closed=closed) + index = IntervalIndex.from_breaks(breaks, inclusive=closed) result = index.length expected = Index(iv.length for iv in index) tm.assert_index_equal(result, expected) @@ -105,7 +105,7 @@ def test_length(self, closed, breaks): tm.assert_index_equal(result, expected) def test_with_nans(self, closed): - index = self.create_index(closed=closed) + index = self.create_index(inclusive=closed) assert index.hasnans is False result = index.isna() @@ -116,7 +116,7 @@ def test_with_nans(self, closed): expected = np.ones(len(index), dtype=bool) tm.assert_numpy_array_equal(result, expected) - index = self.create_index_with_nan(closed=closed) + index = self.create_index_with_nan(inclusive=closed) assert index.hasnans is True result = index.isna() @@ -128,7 +128,7 @@ def test_with_nans(self, closed): tm.assert_numpy_array_equal(result, expected) def test_copy(self, closed): - expected = self.create_index(closed=closed) + expected = self.create_index(inclusive=closed) result = expected.copy() assert result.equals(expected) @@ -141,7 +141,7 @@ def test_ensure_copied_data(self, closed): # exercise the copy flag in the constructor # not copying - index = self.create_index(closed=closed) + index = self.create_index(inclusive=closed) result = IntervalIndex(index, copy=False) tm.assert_numpy_array_equal( index.left.values, result.left.values, check_same="same" @@ -160,8 +160,8 @@ def test_ensure_copied_data(self, closed): ) def test_delete(self, closed): - expected = IntervalIndex.from_breaks(np.arange(1, 11), closed=closed) - result = self.create_index(closed=closed).delete(0) + expected = IntervalIndex.from_breaks(np.arange(1, 11), inclusive=closed) + result = self.create_index(inclusive=closed).delete(0) tm.assert_index_equal(result, expected) @pytest.mark.parametrize( @@ -201,11 +201,11 @@ def test_insert(self, data): with pytest.raises(TypeError, match=msg): data._data.insert(1, "foo") - # invalid closed - msg = "'value.closed' is 'left', expected 'right'." - for closed in {"left", "right", "both", "neither"} - {item.closed}: - msg = f"'value.closed' is '{closed}', expected '{item.closed}'." - bad_item = Interval(item.left, item.right, closed=closed) + # invalid inclusive + msg = "'value.inclusive' is 'left', expected 'right'." + for inclusive in {"left", "right", "both", "neither"} - {item.inclusive}: + msg = f"'value.inclusive' is '{inclusive}', expected '{item.inclusive}'." + bad_item = Interval(item.left, item.right, inclusive=inclusive) res = data.insert(1, bad_item) expected = data.astype(object).insert(1, bad_item) tm.assert_index_equal(res, expected) @@ -213,7 +213,7 @@ def test_insert(self, data): data._data.insert(1, bad_item) # GH 18295 (test missing) - na_idx = IntervalIndex([np.nan], closed=data.closed) + na_idx = IntervalIndex([np.nan], inclusive=data.inclusive) for na in [np.nan, None, pd.NA]: expected = data[:1].append(na_idx).append(data[1:]) result = data.insert(1, na) @@ -235,93 +235,93 @@ def test_is_unique_interval(self, closed): Interval specific tests for is_unique in addition to base class tests """ # unique overlapping - distinct endpoints - idx = IntervalIndex.from_tuples([(0, 1), (0.5, 1.5)], closed=closed) + idx = IntervalIndex.from_tuples([(0, 1), (0.5, 1.5)], inclusive=closed) assert idx.is_unique is True # unique overlapping - shared endpoints - idx = IntervalIndex.from_tuples([(1, 2), (1, 3), (2, 3)], closed=closed) + idx = IntervalIndex.from_tuples([(1, 2), (1, 3), (2, 3)], inclusive=closed) assert idx.is_unique is True # unique nested - idx = IntervalIndex.from_tuples([(-1, 1), (-2, 2)], closed=closed) + idx = IntervalIndex.from_tuples([(-1, 1), (-2, 2)], inclusive=closed) assert idx.is_unique is True # unique NaN - idx = IntervalIndex.from_tuples([(np.NaN, np.NaN)], closed=closed) + idx = IntervalIndex.from_tuples([(np.NaN, np.NaN)], inclusive=closed) assert idx.is_unique is True # non-unique NaN idx = IntervalIndex.from_tuples( - [(np.NaN, np.NaN), (np.NaN, np.NaN)], closed=closed + [(np.NaN, np.NaN), (np.NaN, np.NaN)], inclusive=closed ) assert idx.is_unique is False def test_monotonic(self, closed): # increasing non-overlapping - idx = IntervalIndex.from_tuples([(0, 1), (2, 3), (4, 5)], closed=closed) + idx = IntervalIndex.from_tuples([(0, 1), (2, 3), (4, 5)], inclusive=closed) assert idx.is_monotonic_increasing is True assert idx._is_strictly_monotonic_increasing is True assert idx.is_monotonic_decreasing is False assert idx._is_strictly_monotonic_decreasing is False # decreasing non-overlapping - idx = IntervalIndex.from_tuples([(4, 5), (2, 3), (1, 2)], closed=closed) + idx = IntervalIndex.from_tuples([(4, 5), (2, 3), (1, 2)], inclusive=closed) assert idx.is_monotonic_increasing is False assert idx._is_strictly_monotonic_increasing is False assert idx.is_monotonic_decreasing is True assert idx._is_strictly_monotonic_decreasing is True # unordered non-overlapping - idx = IntervalIndex.from_tuples([(0, 1), (4, 5), (2, 3)], closed=closed) + idx = IntervalIndex.from_tuples([(0, 1), (4, 5), (2, 3)], inclusive=closed) assert idx.is_monotonic_increasing is False assert idx._is_strictly_monotonic_increasing is False assert idx.is_monotonic_decreasing is False assert idx._is_strictly_monotonic_decreasing is False # increasing overlapping - idx = IntervalIndex.from_tuples([(0, 2), (0.5, 2.5), (1, 3)], closed=closed) + idx = IntervalIndex.from_tuples([(0, 2), (0.5, 2.5), (1, 3)], inclusive=closed) assert idx.is_monotonic_increasing is True assert idx._is_strictly_monotonic_increasing is True assert idx.is_monotonic_decreasing is False assert idx._is_strictly_monotonic_decreasing is False # decreasing overlapping - idx = IntervalIndex.from_tuples([(1, 3), (0.5, 2.5), (0, 2)], closed=closed) + idx = IntervalIndex.from_tuples([(1, 3), (0.5, 2.5), (0, 2)], inclusive=closed) assert idx.is_monotonic_increasing is False assert idx._is_strictly_monotonic_increasing is False assert idx.is_monotonic_decreasing is True assert idx._is_strictly_monotonic_decreasing is True # unordered overlapping - idx = IntervalIndex.from_tuples([(0.5, 2.5), (0, 2), (1, 3)], closed=closed) + idx = IntervalIndex.from_tuples([(0.5, 2.5), (0, 2), (1, 3)], inclusive=closed) assert idx.is_monotonic_increasing is False assert idx._is_strictly_monotonic_increasing is False assert idx.is_monotonic_decreasing is False assert idx._is_strictly_monotonic_decreasing is False # increasing overlapping shared endpoints - idx = IntervalIndex.from_tuples([(1, 2), (1, 3), (2, 3)], closed=closed) + idx = IntervalIndex.from_tuples([(1, 2), (1, 3), (2, 3)], inclusive=closed) assert idx.is_monotonic_increasing is True assert idx._is_strictly_monotonic_increasing is True assert idx.is_monotonic_decreasing is False assert idx._is_strictly_monotonic_decreasing is False # decreasing overlapping shared endpoints - idx = IntervalIndex.from_tuples([(2, 3), (1, 3), (1, 2)], closed=closed) + idx = IntervalIndex.from_tuples([(2, 3), (1, 3), (1, 2)], inclusive=closed) assert idx.is_monotonic_increasing is False assert idx._is_strictly_monotonic_increasing is False assert idx.is_monotonic_decreasing is True assert idx._is_strictly_monotonic_decreasing is True # stationary - idx = IntervalIndex.from_tuples([(0, 1), (0, 1)], closed=closed) + idx = IntervalIndex.from_tuples([(0, 1), (0, 1)], inclusive=closed) assert idx.is_monotonic_increasing is True assert idx._is_strictly_monotonic_increasing is False assert idx.is_monotonic_decreasing is True assert idx._is_strictly_monotonic_decreasing is False # empty - idx = IntervalIndex([], closed=closed) + idx = IntervalIndex([], inclusive=closed) assert idx.is_monotonic_increasing is True assert idx._is_strictly_monotonic_increasing is True assert idx.is_monotonic_decreasing is True @@ -338,22 +338,22 @@ def test_is_monotonic_with_nans(self): assert not index.is_monotonic_decreasing def test_get_item(self, closed): - i = IntervalIndex.from_arrays((0, 1, np.nan), (1, 2, np.nan), closed=closed) - assert i[0] == Interval(0.0, 1.0, closed=closed) - assert i[1] == Interval(1.0, 2.0, closed=closed) + i = IntervalIndex.from_arrays((0, 1, np.nan), (1, 2, np.nan), inclusive=closed) + assert i[0] == Interval(0.0, 1.0, inclusive=closed) + assert i[1] == Interval(1.0, 2.0, inclusive=closed) assert isna(i[2]) result = i[0:1] - expected = IntervalIndex.from_arrays((0.0,), (1.0,), closed=closed) + expected = IntervalIndex.from_arrays((0.0,), (1.0,), inclusive=closed) tm.assert_index_equal(result, expected) result = i[0:2] - expected = IntervalIndex.from_arrays((0.0, 1), (1.0, 2.0), closed=closed) + expected = IntervalIndex.from_arrays((0.0, 1), (1.0, 2.0), inclusive=closed) tm.assert_index_equal(result, expected) result = i[1:3] expected = IntervalIndex.from_arrays( - (1.0, np.nan), (2.0, np.nan), closed=closed + (1.0, np.nan), (2.0, np.nan), inclusive=closed ) tm.assert_index_equal(result, expected) @@ -477,7 +477,7 @@ def test_maybe_convert_i8_errors(self, breaks1, breaks2, make_key): def test_contains_method(self): # can select values that are IN the range of a value - i = IntervalIndex.from_arrays([0, 1], [1, 2]) + i = IntervalIndex.from_arrays([0, 1], [1, 2], "right") expected = np.array([False, False], dtype="bool") actual = i.contains(0) @@ -500,18 +500,18 @@ def test_contains_method(self): def test_dropna(self, closed): - expected = IntervalIndex.from_tuples([(0.0, 1.0), (1.0, 2.0)], closed=closed) + expected = IntervalIndex.from_tuples([(0.0, 1.0), (1.0, 2.0)], inclusive=closed) - ii = IntervalIndex.from_tuples([(0, 1), (1, 2), np.nan], closed=closed) + ii = IntervalIndex.from_tuples([(0, 1), (1, 2), np.nan], inclusive=closed) result = ii.dropna() tm.assert_index_equal(result, expected) - ii = IntervalIndex.from_arrays([0, 1, np.nan], [1, 2, np.nan], closed=closed) + ii = IntervalIndex.from_arrays([0, 1, np.nan], [1, 2, np.nan], inclusive=closed) result = ii.dropna() tm.assert_index_equal(result, expected) def test_non_contiguous(self, closed): - index = IntervalIndex.from_tuples([(0, 1), (2, 3)], closed=closed) + index = IntervalIndex.from_tuples([(0, 1), (2, 3)], inclusive=closed) target = [0.5, 1.5, 2.5] actual = index.get_indexer(target) expected = np.array([0, -1, 1], dtype="intp") @@ -520,7 +520,7 @@ def test_non_contiguous(self, closed): assert 1.5 not in index def test_isin(self, closed): - index = self.create_index(closed=closed) + index = self.create_index(inclusive=closed) expected = np.array([True] + [False] * (len(index) - 1)) result = index.isin(index[:1]) @@ -529,7 +529,7 @@ def test_isin(self, closed): result = index.isin([index[0]]) tm.assert_numpy_array_equal(result, expected) - other = IntervalIndex.from_breaks(np.arange(-2, 10), closed=closed) + other = IntervalIndex.from_breaks(np.arange(-2, 10), inclusive=closed) expected = np.array([True] * (len(index) - 1) + [False]) result = index.isin(other) tm.assert_numpy_array_equal(result, expected) @@ -537,9 +537,9 @@ def test_isin(self, closed): result = index.isin(other.tolist()) tm.assert_numpy_array_equal(result, expected) - for other_closed in {"right", "left", "both", "neither"}: - other = self.create_index(closed=other_closed) - expected = np.repeat(closed == other_closed, len(index)) + for other_inclusive in {"right", "left", "both", "neither"}: + other = self.create_index(inclusive=other_inclusive) + expected = np.repeat(closed == other_inclusive, len(index)) result = index.isin(other) tm.assert_numpy_array_equal(result, expected) @@ -547,14 +547,14 @@ def test_isin(self, closed): tm.assert_numpy_array_equal(result, expected) def test_comparison(self): - actual = Interval(0, 1) < self.index + actual = Interval(0, 1, "right") < self.index expected = np.array([False, True]) tm.assert_numpy_array_equal(actual, expected) - actual = Interval(0.5, 1.5) < self.index + actual = Interval(0.5, 1.5, "right") < self.index expected = np.array([False, True]) tm.assert_numpy_array_equal(actual, expected) - actual = self.index > Interval(0.5, 1.5) + actual = self.index > Interval(0.5, 1.5, "right") tm.assert_numpy_array_equal(actual, expected) actual = self.index == self.index @@ -612,9 +612,11 @@ def test_comparison(self): def test_missing_values(self, closed): idx = Index( - [np.nan, Interval(0, 1, closed=closed), Interval(1, 2, closed=closed)] + [np.nan, Interval(0, 1, inclusive=closed), Interval(1, 2, inclusive=closed)] + ) + idx2 = IntervalIndex.from_arrays( + [np.nan, 0, 1], [np.nan, 1, 2], inclusive=closed ) - idx2 = IntervalIndex.from_arrays([np.nan, 0, 1], [np.nan, 1, 2], closed=closed) assert idx.equals(idx2) msg = ( @@ -623,13 +625,13 @@ def test_missing_values(self, closed): ) with pytest.raises(ValueError, match=msg): IntervalIndex.from_arrays( - [np.nan, 0, 1], np.array([0, 1, 2]), closed=closed + [np.nan, 0, 1], np.array([0, 1, 2]), inclusive=closed ) tm.assert_numpy_array_equal(isna(idx), np.array([True, False, False])) def test_sort_values(self, closed): - index = self.create_index(closed=closed) + index = self.create_index(inclusive=closed) result = index.sort_values() tm.assert_index_equal(result, index) @@ -652,7 +654,7 @@ def test_sort_values(self, closed): def test_datetime(self, tz): start = Timestamp("2000-01-01", tz=tz) dates = date_range(start=start, periods=10) - index = IntervalIndex.from_breaks(dates) + index = IntervalIndex.from_breaks(dates, "right") # test mid start = Timestamp("2000-01-01T12:00", tz=tz) @@ -664,10 +666,10 @@ def test_datetime(self, tz): assert Timestamp("2000-01-01T12", tz=tz) not in index assert Timestamp("2000-01-02", tz=tz) not in index iv_true = Interval( - Timestamp("2000-01-02", tz=tz), Timestamp("2000-01-03", tz=tz) + Timestamp("2000-01-02", tz=tz), Timestamp("2000-01-03", tz=tz), "right" ) iv_false = Interval( - Timestamp("1999-12-31", tz=tz), Timestamp("2000-01-01", tz=tz) + Timestamp("1999-12-31", tz=tz), Timestamp("2000-01-01", tz=tz), "right" ) assert iv_true in index assert iv_false not in index @@ -692,58 +694,62 @@ def test_datetime(self, tz): def test_append(self, closed): - index1 = IntervalIndex.from_arrays([0, 1], [1, 2], closed=closed) - index2 = IntervalIndex.from_arrays([1, 2], [2, 3], closed=closed) + index1 = IntervalIndex.from_arrays([0, 1], [1, 2], inclusive=closed) + index2 = IntervalIndex.from_arrays([1, 2], [2, 3], inclusive=closed) result = index1.append(index2) - expected = IntervalIndex.from_arrays([0, 1, 1, 2], [1, 2, 2, 3], closed=closed) + expected = IntervalIndex.from_arrays( + [0, 1, 1, 2], [1, 2, 2, 3], inclusive=closed + ) tm.assert_index_equal(result, expected) result = index1.append([index1, index2]) expected = IntervalIndex.from_arrays( - [0, 1, 0, 1, 1, 2], [1, 2, 1, 2, 2, 3], closed=closed + [0, 1, 0, 1, 1, 2], [1, 2, 1, 2, 2, 3], inclusive=closed ) tm.assert_index_equal(result, expected) - for other_closed in {"left", "right", "both", "neither"} - {closed}: - index_other_closed = IntervalIndex.from_arrays( - [0, 1], [1, 2], closed=other_closed + for other_inclusive in {"left", "right", "both", "neither"} - {closed}: + index_other_inclusive = IntervalIndex.from_arrays( + [0, 1], [1, 2], inclusive=other_inclusive + ) + result = index1.append(index_other_inclusive) + expected = index1.astype(object).append( + index_other_inclusive.astype(object) ) - result = index1.append(index_other_closed) - expected = index1.astype(object).append(index_other_closed.astype(object)) tm.assert_index_equal(result, expected) def test_is_non_overlapping_monotonic(self, closed): # Should be True in all cases tpls = [(0, 1), (2, 3), (4, 5), (6, 7)] - idx = IntervalIndex.from_tuples(tpls, closed=closed) + idx = IntervalIndex.from_tuples(tpls, inclusive=closed) assert idx.is_non_overlapping_monotonic is True - idx = IntervalIndex.from_tuples(tpls[::-1], closed=closed) + idx = IntervalIndex.from_tuples(tpls[::-1], inclusive=closed) assert idx.is_non_overlapping_monotonic is True # Should be False in all cases (overlapping) tpls = [(0, 2), (1, 3), (4, 5), (6, 7)] - idx = IntervalIndex.from_tuples(tpls, closed=closed) + idx = IntervalIndex.from_tuples(tpls, inclusive=closed) assert idx.is_non_overlapping_monotonic is False - idx = IntervalIndex.from_tuples(tpls[::-1], closed=closed) + idx = IntervalIndex.from_tuples(tpls[::-1], inclusive=closed) assert idx.is_non_overlapping_monotonic is False # Should be False in all cases (non-monotonic) tpls = [(0, 1), (2, 3), (6, 7), (4, 5)] - idx = IntervalIndex.from_tuples(tpls, closed=closed) + idx = IntervalIndex.from_tuples(tpls, inclusive=closed) assert idx.is_non_overlapping_monotonic is False - idx = IntervalIndex.from_tuples(tpls[::-1], closed=closed) + idx = IntervalIndex.from_tuples(tpls[::-1], inclusive=closed) assert idx.is_non_overlapping_monotonic is False - # Should be False for closed='both', otherwise True (GH16560) + # Should be False for inclusive='both', otherwise True (GH16560) if closed == "both": - idx = IntervalIndex.from_breaks(range(4), closed=closed) + idx = IntervalIndex.from_breaks(range(4), inclusive=closed) assert idx.is_non_overlapping_monotonic is False else: - idx = IntervalIndex.from_breaks(range(4), closed=closed) + idx = IntervalIndex.from_breaks(range(4), inclusive=closed) assert idx.is_non_overlapping_monotonic is True @pytest.mark.parametrize( @@ -760,34 +766,34 @@ def test_is_overlapping(self, start, shift, na_value, closed): # non-overlapping tuples = [(start + n * shift, start + (n + 1) * shift) for n in (0, 2, 4)] - index = IntervalIndex.from_tuples(tuples, closed=closed) + index = IntervalIndex.from_tuples(tuples, inclusive=closed) assert index.is_overlapping is False # non-overlapping with NA tuples = [(na_value, na_value)] + tuples + [(na_value, na_value)] - index = IntervalIndex.from_tuples(tuples, closed=closed) + index = IntervalIndex.from_tuples(tuples, inclusive=closed) assert index.is_overlapping is False # overlapping tuples = [(start + n * shift, start + (n + 2) * shift) for n in range(3)] - index = IntervalIndex.from_tuples(tuples, closed=closed) + index = IntervalIndex.from_tuples(tuples, inclusive=closed) assert index.is_overlapping is True # overlapping with NA tuples = [(na_value, na_value)] + tuples + [(na_value, na_value)] - index = IntervalIndex.from_tuples(tuples, closed=closed) + index = IntervalIndex.from_tuples(tuples, inclusive=closed) assert index.is_overlapping is True # common endpoints tuples = [(start + n * shift, start + (n + 1) * shift) for n in range(3)] - index = IntervalIndex.from_tuples(tuples, closed=closed) + index = IntervalIndex.from_tuples(tuples, inclusive=closed) result = index.is_overlapping expected = closed == "both" assert result is expected # common endpoints with NA tuples = [(na_value, na_value)] + tuples + [(na_value, na_value)] - index = IntervalIndex.from_tuples(tuples, closed=closed) + index = IntervalIndex.from_tuples(tuples, inclusive=closed) result = index.is_overlapping assert result is expected @@ -873,13 +879,13 @@ def test_set_closed(self, name, closed, new_closed): expected = interval_range(0, 5, inclusive=new_closed, name=name) tm.assert_index_equal(result, expected) - @pytest.mark.parametrize("bad_closed", ["foo", 10, "LEFT", True, False]) - def test_set_closed_errors(self, bad_closed): + @pytest.mark.parametrize("bad_inclusive", ["foo", 10, "LEFT", True, False]) + def test_set_closed_errors(self, bad_inclusive): # GH 21670 index = interval_range(0, 5) - msg = f"invalid option for 'closed': {bad_closed}" + msg = f"invalid option for 'inclusive': {bad_inclusive}" with pytest.raises(ValueError, match=msg): - index.set_closed(bad_closed) + index.set_closed(bad_inclusive) def test_is_all_dates(self): # GH 23576 @@ -889,6 +895,39 @@ def test_is_all_dates(self): year_2017_index = IntervalIndex([year_2017]) assert not year_2017_index._is_all_dates + def test_interval_index_error_and_warning(self): + # GH 40245 + msg = ( + "Deprecated argument `closed` cannot " + "be passed if argument `inclusive` is not None" + ) + with pytest.raises(ValueError, match=msg): + IntervalIndex.from_breaks(range(11), closed="both", inclusive="both") + + with pytest.raises(ValueError, match=msg): + IntervalIndex.from_arrays([0, 1], [1, 2], closed="both", inclusive="both") + + with pytest.raises(ValueError, match=msg): + IntervalIndex.from_tuples( + [(0, 1), (0.5, 1.5)], closed="both", inclusive="both" + ) + + msg = "Argument `closed` is deprecated in favor of `inclusive`" + with tm.assert_produces_warning( + FutureWarning, match=msg, check_stacklevel=False + ): + IntervalIndex.from_breaks(range(11), closed="both") + + with tm.assert_produces_warning( + FutureWarning, match=msg, check_stacklevel=False + ): + IntervalIndex.from_arrays([0, 1], [1, 2], closed="both") + + with tm.assert_produces_warning( + FutureWarning, match=msg, check_stacklevel=False + ): + IntervalIndex.from_tuples([(0, 1), (0.5, 1.5)], closed="both") + def test_dir(): # GH#27571 dir(interval_index) should not raise diff --git a/pandas/tests/indexes/interval/test_interval_range.py b/pandas/tests/indexes/interval/test_interval_range.py index 63e7f3aa2b120..255470cf4683e 100644 --- a/pandas/tests/indexes/interval/test_interval_range.py +++ b/pandas/tests/indexes/interval/test_interval_range.py @@ -30,7 +30,7 @@ class TestIntervalRange: def test_constructor_numeric(self, closed, name, freq, periods): start, end = 0, 100 breaks = np.arange(101, step=freq) - expected = IntervalIndex.from_breaks(breaks, name=name, closed=closed) + expected = IntervalIndex.from_breaks(breaks, name=name, inclusive=closed) # defined from start/end/freq result = interval_range( @@ -63,7 +63,7 @@ def test_constructor_numeric(self, closed, name, freq, periods): def test_constructor_timestamp(self, closed, name, freq, periods, tz): start, end = Timestamp("20180101", tz=tz), Timestamp("20181231", tz=tz) breaks = date_range(start=start, end=end, freq=freq) - expected = IntervalIndex.from_breaks(breaks, name=name, closed=closed) + expected = IntervalIndex.from_breaks(breaks, name=name, inclusive=closed) # defined from start/end/freq result = interval_range( @@ -98,7 +98,7 @@ def test_constructor_timestamp(self, closed, name, freq, periods, tz): def test_constructor_timedelta(self, closed, name, freq, periods): start, end = Timedelta("0 days"), Timedelta("100 days") breaks = timedelta_range(start=start, end=end, freq=freq) - expected = IntervalIndex.from_breaks(breaks, name=name, closed=closed) + expected = IntervalIndex.from_breaks(breaks, name=name, inclusive=closed) # defined from start/end/freq result = interval_range( @@ -161,7 +161,7 @@ def test_no_invalid_float_truncation(self, start, end, freq): breaks = [0.5, 1.5, 2.5, 3.5, 4.5] else: breaks = [0.5, 2.0, 3.5, 5.0, 6.5] - expected = IntervalIndex.from_breaks(breaks) + expected = IntervalIndex.from_breaks(breaks, "right") result = interval_range( start=start, end=end, periods=4, freq=freq, inclusive="right" @@ -187,7 +187,8 @@ def test_linspace_dst_transition(self, start, mid, end): # GH 20976: linspace behavior defined from start/end/periods # accounts for the hour gained/lost during DST transition result = interval_range(start=start, end=end, periods=2, inclusive="right") - expected = IntervalIndex.from_breaks([start, mid, end]) + expected = IntervalIndex.from_breaks([start, mid, end], "right") + tm.assert_index_equal(result, expected) @pytest.mark.parametrize("freq", [2, 2.0]) @@ -336,7 +337,7 @@ def test_errors(self): # invalid end msg = r"end must be numeric or datetime-like, got \(0, 1\]" with pytest.raises(ValueError, match=msg): - interval_range(end=Interval(0, 1), periods=10) + interval_range(end=Interval(0, 1, "right"), periods=10) # invalid freq for datetime-like msg = "freq must be numeric or convertible to DateOffset, got foo" diff --git a/pandas/tests/indexes/interval/test_interval_tree.py b/pandas/tests/indexes/interval/test_interval_tree.py index f2d9ec3608271..345025d63f4b2 100644 --- a/pandas/tests/indexes/interval/test_interval_tree.py +++ b/pandas/tests/indexes/interval/test_interval_tree.py @@ -42,7 +42,7 @@ def leaf_size(request): ) def tree(request, leaf_size): left = request.param - return IntervalTree(left, left + 2, leaf_size=leaf_size) + return IntervalTree(left, left + 2, leaf_size=leaf_size, inclusive="right") class TestIntervalTree: @@ -129,7 +129,7 @@ def test_get_indexer_closed(self, closed, leaf_size): found = x.astype("intp") not_found = (-1 * np.ones(1000)).astype("intp") - tree = IntervalTree(x, x + 0.5, closed=closed, leaf_size=leaf_size) + tree = IntervalTree(x, x + 0.5, inclusive=closed, leaf_size=leaf_size) tm.assert_numpy_array_equal(found, tree.get_indexer(x + 0.25)) expected = found if tree.closed_left else not_found @@ -151,7 +151,7 @@ def test_get_indexer_closed(self, closed, leaf_size): @pytest.mark.parametrize("order", (list(x) for x in permutations(range(3)))) def test_is_overlapping(self, closed, order, left, right, expected): # GH 23309 - tree = IntervalTree(left[order], right[order], closed=closed) + tree = IntervalTree(left[order], right[order], inclusive=closed) result = tree.is_overlapping assert result is expected @@ -160,7 +160,7 @@ def test_is_overlapping_endpoints(self, closed, order): """shared endpoints are marked as overlapping""" # GH 23309 left, right = np.arange(3, dtype="int64"), np.arange(1, 4) - tree = IntervalTree(left[order], right[order], closed=closed) + tree = IntervalTree(left[order], right[order], inclusive=closed) result = tree.is_overlapping expected = closed == "both" assert result is expected @@ -176,7 +176,7 @@ def test_is_overlapping_endpoints(self, closed, order): ) def test_is_overlapping_trivial(self, closed, left, right): # GH 23309 - tree = IntervalTree(left, right, closed=closed) + tree = IntervalTree(left, right, inclusive=closed) assert tree.is_overlapping is False @pytest.mark.skipif(not IS64, reason="GH 23440") @@ -189,3 +189,21 @@ def test_construction_overflow(self): result = tree.root.pivot expected = (50 + np.iinfo(np.int64).max) / 2 assert result == expected + + def test_interval_tree_error_and_warning(self): + # GH 40245 + + msg = ( + "Deprecated argument `closed` cannot " + "be passed if argument `inclusive` is not None" + ) + with pytest.raises(ValueError, match=msg): + left, right = np.arange(10), [np.iinfo(np.int64).max] * 10 + IntervalTree(left, right, closed="both", inclusive="both") + + msg = "Argument `closed` is deprecated in favor of `inclusive`" + with tm.assert_produces_warning( + FutureWarning, match=msg, check_stacklevel=False + ): + left, right = np.arange(10), [np.iinfo(np.int64).max] * 10 + IntervalTree(left, right, closed="both") diff --git a/pandas/tests/indexes/interval/test_pickle.py b/pandas/tests/indexes/interval/test_pickle.py index 308a90e72eab5..7f5784b6d76b9 100644 --- a/pandas/tests/indexes/interval/test_pickle.py +++ b/pandas/tests/indexes/interval/test_pickle.py @@ -5,9 +5,9 @@ class TestPickle: - @pytest.mark.parametrize("closed", ["left", "right", "both"]) - def test_pickle_round_trip_closed(self, closed): + @pytest.mark.parametrize("inclusive", ["left", "right", "both"]) + def test_pickle_round_trip_closed(self, inclusive): # https://github.com/pandas-dev/pandas/issues/35658 - idx = IntervalIndex.from_tuples([(1, 2), (2, 3)], closed=closed) + idx = IntervalIndex.from_tuples([(1, 2), (2, 3)], inclusive=inclusive) result = tm.round_trip_pickle(idx) tm.assert_index_equal(result, idx) diff --git a/pandas/tests/indexes/interval/test_setops.py b/pandas/tests/indexes/interval/test_setops.py index 51a1d36398aa4..5933961cc0f9d 100644 --- a/pandas/tests/indexes/interval/test_setops.py +++ b/pandas/tests/indexes/interval/test_setops.py @@ -11,11 +11,13 @@ def monotonic_index(start, end, dtype="int64", closed="right"): - return IntervalIndex.from_breaks(np.arange(start, end, dtype=dtype), closed=closed) + return IntervalIndex.from_breaks( + np.arange(start, end, dtype=dtype), inclusive=closed + ) def empty_index(dtype="int64", closed="right"): - return IntervalIndex(np.array([], dtype=dtype), closed=closed) + return IntervalIndex(np.array([], dtype=dtype), inclusive=closed) class TestIntervalIndex: @@ -125,7 +127,7 @@ def test_intersection_duplicates(self): tm.assert_index_equal(result, expected) def test_difference(self, closed, sort): - index = IntervalIndex.from_arrays([1, 0, 3, 2], [1, 2, 3, 4], closed=closed) + index = IntervalIndex.from_arrays([1, 0, 3, 2], [1, 2, 3, 4], inclusive=closed) result = index.difference(index[:1], sort=sort) expected = index[1:] if sort is None: @@ -139,7 +141,7 @@ def test_difference(self, closed, sort): # GH 19101: empty result, different dtypes other = IntervalIndex.from_arrays( - index.left.astype("float64"), index.right, closed=closed + index.left.astype("float64"), index.right, inclusive=closed ) result = index.difference(other, sort=sort) tm.assert_index_equal(result, expected) @@ -161,7 +163,7 @@ def test_symmetric_difference(self, closed, sort): # GH 19101: empty result, different dtypes other = IntervalIndex.from_arrays( - index.left.astype("float64"), index.right, closed=closed + index.left.astype("float64"), index.right, inclusive=closed ) result = index.symmetric_difference(other, sort=sort) expected = empty_index(dtype="float64", closed=closed) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index 55f3e27be5a72..943cc945995a1 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -1431,10 +1431,10 @@ def test_ensure_index_from_sequences(self, data, names, expected): def test_ensure_index_mixed_closed_intervals(self): # GH27172 intervals = [ - pd.Interval(0, 1, closed="left"), - pd.Interval(1, 2, closed="right"), - pd.Interval(2, 3, closed="neither"), - pd.Interval(3, 4, closed="both"), + pd.Interval(0, 1, inclusive="left"), + pd.Interval(1, 2, inclusive="right"), + pd.Interval(2, 3, inclusive="neither"), + pd.Interval(3, 4, inclusive="both"), ] result = ensure_index(intervals) expected = Index(intervals, dtype=object) diff --git a/pandas/tests/indexing/interval/test_interval.py b/pandas/tests/indexing/interval/test_interval.py index db3a569d3925b..7d1f1ef09fc5d 100644 --- a/pandas/tests/indexing/interval/test_interval.py +++ b/pandas/tests/indexing/interval/test_interval.py @@ -13,7 +13,7 @@ class TestIntervalIndex: @pytest.fixture def series_with_interval_index(self): - return Series(np.arange(5), IntervalIndex.from_breaks(np.arange(6))) + return Series(np.arange(5), IntervalIndex.from_breaks(np.arange(6), "right")) def test_getitem_with_scalar(self, series_with_interval_index, indexer_sl): @@ -40,7 +40,7 @@ def test_getitem_nonoverlapping_monotonic(self, direction, closed, indexer_sl): if direction == "decreasing": tpls = tpls[::-1] - idx = IntervalIndex.from_tuples(tpls, closed=closed) + idx = IntervalIndex.from_tuples(tpls, inclusive=closed) ser = Series(list("abc"), idx) for key, expected in zip(idx.left, ser): diff --git a/pandas/tests/indexing/interval/test_interval_new.py b/pandas/tests/indexing/interval/test_interval_new.py index aad6523357df6..2e3c765b2b372 100644 --- a/pandas/tests/indexing/interval/test_interval_new.py +++ b/pandas/tests/indexing/interval/test_interval_new.py @@ -14,7 +14,9 @@ class TestIntervalIndex: @pytest.fixture def series_with_interval_index(self): - return Series(np.arange(5), IntervalIndex.from_breaks(np.arange(6))) + return Series( + np.arange(5), IntervalIndex.from_breaks(np.arange(6), inclusive="right") + ) def test_loc_with_interval(self, series_with_interval_index, indexer_sl): @@ -25,27 +27,33 @@ def test_loc_with_interval(self, series_with_interval_index, indexer_sl): ser = series_with_interval_index.copy() expected = 0 - result = indexer_sl(ser)[Interval(0, 1)] + result = indexer_sl(ser)[Interval(0, 1, "right")] assert result == expected expected = ser.iloc[3:5] - result = indexer_sl(ser)[[Interval(3, 4), Interval(4, 5)]] + result = indexer_sl(ser)[[Interval(3, 4, "right"), Interval(4, 5, "right")]] tm.assert_series_equal(expected, result) # missing or not exact - with pytest.raises(KeyError, match=re.escape("Interval(3, 5, closed='left')")): - indexer_sl(ser)[Interval(3, 5, closed="left")] + with pytest.raises( + KeyError, match=re.escape("Interval(3, 5, inclusive='left')") + ): + indexer_sl(ser)[Interval(3, 5, inclusive="left")] - with pytest.raises(KeyError, match=re.escape("Interval(3, 5, closed='right')")): - indexer_sl(ser)[Interval(3, 5)] + with pytest.raises( + KeyError, match=re.escape("Interval(3, 5, inclusive='right')") + ): + indexer_sl(ser)[Interval(3, 5, "right")] with pytest.raises( - KeyError, match=re.escape("Interval(-2, 0, closed='right')") + KeyError, match=re.escape("Interval(-2, 0, inclusive='right')") ): - indexer_sl(ser)[Interval(-2, 0)] + indexer_sl(ser)[Interval(-2, 0, "right")] - with pytest.raises(KeyError, match=re.escape("Interval(5, 6, closed='right')")): - indexer_sl(ser)[Interval(5, 6)] + with pytest.raises( + KeyError, match=re.escape("Interval(5, 6, inclusive='right')") + ): + indexer_sl(ser)[Interval(5, 6, "right")] def test_loc_with_scalar(self, series_with_interval_index, indexer_sl): @@ -84,11 +92,11 @@ def test_loc_with_slices(self, series_with_interval_index, indexer_sl): # slice of interval expected = ser.iloc[:3] - result = indexer_sl(ser)[Interval(0, 1) : Interval(2, 3)] + result = indexer_sl(ser)[Interval(0, 1, "right") : Interval(2, 3, "right")] tm.assert_series_equal(expected, result) expected = ser.iloc[3:] - result = indexer_sl(ser)[Interval(3, 4) :] + result = indexer_sl(ser)[Interval(3, 4, "right") :] tm.assert_series_equal(expected, result) msg = "Interval objects are not currently supported" @@ -96,7 +104,7 @@ def test_loc_with_slices(self, series_with_interval_index, indexer_sl): indexer_sl(ser)[Interval(3, 6) :] with pytest.raises(NotImplementedError, match=msg): - indexer_sl(ser)[Interval(3, 4, closed="left") :] + indexer_sl(ser)[Interval(3, 4, inclusive="left") :] def test_slice_step_ne1(self, series_with_interval_index): # GH#31658 slice of scalar with step != 1 @@ -127,7 +135,7 @@ def test_slice_interval_step(self, series_with_interval_index): def test_loc_with_overlap(self, indexer_sl): - idx = IntervalIndex.from_tuples([(1, 5), (3, 7)]) + idx = IntervalIndex.from_tuples([(1, 5), (3, 7)], inclusive="right") ser = Series(range(len(idx)), index=idx) # scalar @@ -140,23 +148,25 @@ def test_loc_with_overlap(self, indexer_sl): # interval expected = 0 - result = indexer_sl(ser)[Interval(1, 5)] + result = indexer_sl(ser)[Interval(1, 5, "right")] result == expected expected = ser - result = indexer_sl(ser)[[Interval(1, 5), Interval(3, 7)]] + result = indexer_sl(ser)[[Interval(1, 5, "right"), Interval(3, 7, "right")]] tm.assert_series_equal(expected, result) - with pytest.raises(KeyError, match=re.escape("Interval(3, 5, closed='right')")): - indexer_sl(ser)[Interval(3, 5)] + with pytest.raises( + KeyError, match=re.escape("Interval(3, 5, inclusive='right')") + ): + indexer_sl(ser)[Interval(3, 5, "right")] - msg = r"None of \[\[Interval\(3, 5, closed='right'\)\]\]" + msg = r"None of \[\[Interval\(3, 5, inclusive='right'\)\]\]" with pytest.raises(KeyError, match=msg): - indexer_sl(ser)[[Interval(3, 5)]] + indexer_sl(ser)[[Interval(3, 5, "right")]] # slices with interval (only exact matches) expected = ser - result = indexer_sl(ser)[Interval(1, 5) : Interval(3, 7)] + result = indexer_sl(ser)[Interval(1, 5, "right") : Interval(3, 7, "right")] tm.assert_series_equal(expected, result) msg = "'can only get slices from an IntervalIndex if bounds are" diff --git a/pandas/tests/indexing/test_categorical.py b/pandas/tests/indexing/test_categorical.py index b94323e975cd7..21a14ef8523f1 100644 --- a/pandas/tests/indexing/test_categorical.py +++ b/pandas/tests/indexing/test_categorical.py @@ -114,7 +114,7 @@ def test_slicing(self): df = DataFrame({"value": (np.arange(100) + 1).astype("int64")}) df["D"] = pd.cut(df.value, bins=[0, 25, 50, 75, 100]) - expected = Series([11, Interval(0, 25)], index=["value", "D"], name=10) + expected = Series([11, Interval(0, 25, "right")], index=["value", "D"], name=10) result = df.iloc[10] tm.assert_series_equal(result, expected) @@ -126,7 +126,7 @@ def test_slicing(self): result = df.iloc[10:20] tm.assert_frame_equal(result, expected) - expected = Series([9, Interval(0, 25)], index=["value", "D"], name=8) + expected = Series([9, Interval(0, 25, "right")], index=["value", "D"], name=8) result = df.loc[8] tm.assert_series_equal(result, expected) @@ -495,13 +495,13 @@ def test_loc_and_at_with_categorical_index(self): # numpy object np.array([1, "b", 3.5], dtype=object), # pandas scalars - [Interval(1, 4), Interval(4, 6), Interval(6, 9)], + [Interval(1, 4, "right"), Interval(4, 6, "right"), Interval(6, 9, "right")], [Timestamp(2019, 1, 1), Timestamp(2019, 2, 1), Timestamp(2019, 3, 1)], [Timedelta(1, "d"), Timedelta(2, "d"), Timedelta(3, "D")], # pandas Integer arrays *(pd.array([1, 2, 3], dtype=dtype) for dtype in tm.ALL_INT_EA_DTYPES), # other pandas arrays - pd.IntervalIndex.from_breaks([1, 4, 6, 9]).array, + pd.IntervalIndex.from_breaks([1, 4, 6, 9], "right").array, pd.date_range("2019-01-01", periods=3).array, pd.timedelta_range(start="1d", periods=3).array, ], diff --git a/pandas/tests/indexing/test_coercion.py b/pandas/tests/indexing/test_coercion.py index 4504c55698a9a..be8fcfb4d8348 100644 --- a/pandas/tests/indexing/test_coercion.py +++ b/pandas/tests/indexing/test_coercion.py @@ -701,7 +701,7 @@ def test_fillna_datetime64tz(self, index_or_series, fill_val, fill_dtype): 1.1, 1 + 1j, True, - pd.Interval(1, 2, closed="left"), + pd.Interval(1, 2, inclusive="left"), pd.Timestamp("2012-01-01", tz="US/Eastern"), pd.Timestamp("2012-01-01"), pd.Timedelta(days=1), @@ -745,7 +745,7 @@ def test_fillna_series_timedelta64(self): 1.1, 1 + 1j, True, - pd.Interval(1, 2, closed="left"), + pd.Interval(1, 2, inclusive="left"), pd.Timestamp("2012-01-01", tz="US/Eastern"), pd.Timestamp("2012-01-01"), pd.Timedelta(days=1), diff --git a/pandas/tests/internals/test_internals.py b/pandas/tests/internals/test_internals.py index 3c90eee5be999..2f3b569c899e1 100644 --- a/pandas/tests/internals/test_internals.py +++ b/pandas/tests/internals/test_internals.py @@ -1271,7 +1271,7 @@ def test_interval_can_hold_element(self, dtype, element): # Careful: to get the expected Series-inplace behavior we need # `elem` to not have the same length as `arr` - ii2 = IntervalIndex.from_breaks(arr[:-1], closed="neither") + ii2 = IntervalIndex.from_breaks(arr[:-1], inclusive="neither") elem = element(ii2) self.check_series_setitem(elem, ii, False) assert not blk._can_hold_element(elem) diff --git a/pandas/tests/reshape/concat/test_append.py b/pandas/tests/reshape/concat/test_append.py index 0b1d1c4a3d346..7e4371100b5ad 100644 --- a/pandas/tests/reshape/concat/test_append.py +++ b/pandas/tests/reshape/concat/test_append.py @@ -172,7 +172,7 @@ def test_append_preserve_index_name(self): Index(list("abc")), pd.CategoricalIndex("A B C".split()), pd.CategoricalIndex("D E F".split(), ordered=True), - pd.IntervalIndex.from_breaks([7, 8, 9, 10]), + pd.IntervalIndex.from_breaks([7, 8, 9, 10], inclusive="right"), pd.DatetimeIndex( [ dt.datetime(2013, 1, 3, 0, 0), diff --git a/pandas/tests/reshape/test_cut.py b/pandas/tests/reshape/test_cut.py index 1425686f027e4..815890f319396 100644 --- a/pandas/tests/reshape/test_cut.py +++ b/pandas/tests/reshape/test_cut.py @@ -37,7 +37,7 @@ def test_bins(func): data = func([0.2, 1.4, 2.5, 6.2, 9.7, 2.1]) result, bins = cut(data, 3, retbins=True) - intervals = IntervalIndex.from_breaks(bins.round(3)) + intervals = IntervalIndex.from_breaks(bins.round(3), "right") intervals = intervals.take([0, 0, 0, 1, 2, 0]) expected = Categorical(intervals, ordered=True) @@ -49,7 +49,7 @@ def test_right(): data = np.array([0.2, 1.4, 2.5, 6.2, 9.7, 2.1, 2.575]) result, bins = cut(data, 4, right=True, retbins=True) - intervals = IntervalIndex.from_breaks(bins.round(3)) + intervals = IntervalIndex.from_breaks(bins.round(3), "right") expected = Categorical(intervals, ordered=True) expected = expected.take([0, 0, 0, 2, 3, 0, 0]) @@ -61,7 +61,7 @@ def test_no_right(): data = np.array([0.2, 1.4, 2.5, 6.2, 9.7, 2.1, 2.575]) result, bins = cut(data, 4, right=False, retbins=True) - intervals = IntervalIndex.from_breaks(bins.round(3), closed="left") + intervals = IntervalIndex.from_breaks(bins.round(3), inclusive="left") intervals = intervals.take([0, 0, 0, 2, 3, 0, 1]) expected = Categorical(intervals, ordered=True) @@ -86,7 +86,7 @@ def test_bins_from_interval_index_doc_example(): # Make sure we preserve the bins. ages = np.array([10, 15, 13, 12, 23, 25, 28, 59, 60]) c = cut(ages, bins=[0, 18, 35, 70]) - expected = IntervalIndex.from_tuples([(0, 18), (18, 35), (35, 70)]) + expected = IntervalIndex.from_tuples([(0, 18), (18, 35), (35, 70)], "right") tm.assert_index_equal(c.categories, expected) result = cut([25, 20, 50], bins=c.categories) @@ -121,7 +121,8 @@ def test_bins_not_monotonic(): [ (Timestamp.min, Timestamp("2018-01-01")), (Timestamp("2018-01-01"), Timestamp.max), - ] + ], + "right", ), ), ( @@ -130,7 +131,7 @@ def test_bins_not_monotonic(): [np.iinfo(np.int64).min, 0, np.iinfo(np.int64).max], dtype="int64" ), IntervalIndex.from_tuples( - [(np.iinfo(np.int64).min, 0), (0, np.iinfo(np.int64).max)] + [(np.iinfo(np.int64).min, 0), (0, np.iinfo(np.int64).max)], "right" ), ), ( @@ -156,7 +157,8 @@ def test_bins_not_monotonic(): np.timedelta64(0, "ns"), np.timedelta64(np.iinfo(np.int64).max, "ns"), ), - ] + ], + "right", ), ), ], @@ -232,7 +234,7 @@ def test_labels(right, breaks, closed): arr = np.tile(np.arange(0, 1.01, 0.1), 4) result, bins = cut(arr, 4, retbins=True, right=right) - ex_levels = IntervalIndex.from_breaks(breaks, closed=closed) + ex_levels = IntervalIndex.from_breaks(breaks, inclusive=closed) tm.assert_index_equal(result.categories, ex_levels) @@ -248,7 +250,7 @@ def test_label_precision(): arr = np.arange(0, 0.73, 0.01) result = cut(arr, 4, precision=2) - ex_levels = IntervalIndex.from_breaks([-0.00072, 0.18, 0.36, 0.54, 0.72]) + ex_levels = IntervalIndex.from_breaks([-0.00072, 0.18, 0.36, 0.54, 0.72], "right") tm.assert_index_equal(result.categories, ex_levels) @@ -272,13 +274,13 @@ def test_inf_handling(): result = cut(data, bins) result_ser = cut(data_ser, bins) - ex_uniques = IntervalIndex.from_breaks(bins) + ex_uniques = IntervalIndex.from_breaks(bins, "right") tm.assert_index_equal(result.categories, ex_uniques) - assert result[5] == Interval(4, np.inf) - assert result[0] == Interval(-np.inf, 2) - assert result_ser[5] == Interval(4, np.inf) - assert result_ser[0] == Interval(-np.inf, 2) + assert result[5] == Interval(4, np.inf, "right") + assert result[0] == Interval(-np.inf, 2, "right") + assert result_ser[5] == Interval(4, np.inf, "right") + assert result_ser[0] == Interval(-np.inf, 2, "right") def test_cut_out_of_bounds(): @@ -355,7 +357,7 @@ def test_cut_return_intervals(): exp_bins[0] -= 0.008 expected = Series( - IntervalIndex.from_breaks(exp_bins, closed="right").take( + IntervalIndex.from_breaks(exp_bins, inclusive="right").take( [0, 0, 0, 1, 1, 1, 2, 2, 2] ) ).astype(CDT(ordered=True)) @@ -368,7 +370,7 @@ def test_series_ret_bins(): result, bins = cut(ser, 2, retbins=True) expected = Series( - IntervalIndex.from_breaks([-0.003, 1.5, 3], closed="right").repeat(2) + IntervalIndex.from_breaks([-0.003, 1.5, 3], inclusive="right").repeat(2) ).astype(CDT(ordered=True)) tm.assert_series_equal(result, expected) @@ -442,7 +444,8 @@ def test_datetime_bin(conv): [ Interval(Timestamp(bin_data[0]), Timestamp(bin_data[1])), Interval(Timestamp(bin_data[1]), Timestamp(bin_data[2])), - ] + ], + "right", ) ).astype(CDT(ordered=True)) @@ -488,7 +491,8 @@ def test_datetime_cut(data): Interval( Timestamp("2013-01-02 08:00:00"), Timestamp("2013-01-03 00:00:00") ), - ] + ], + "right", ) ).astype(CDT(ordered=True)) tm.assert_series_equal(Series(result), expected) @@ -531,7 +535,8 @@ def test_datetime_tz_cut(bins, box): Timestamp("2013-01-02 08:00:00", tz=tz), Timestamp("2013-01-03 00:00:00", tz=tz), ), - ] + ], + "right", ) ).astype(CDT(ordered=True)) tm.assert_series_equal(result, expected) @@ -685,8 +690,8 @@ def test_cut_no_warnings(): def test_cut_with_duplicated_index_lowest_included(): # GH 42185 expected = Series( - [Interval(-0.001, 2, closed="right")] * 3 - + [Interval(2, 4, closed="right"), Interval(-0.001, 2, closed="right")], + [Interval(-0.001, 2, inclusive="right")] * 3 + + [Interval(2, 4, inclusive="right"), Interval(-0.001, 2, inclusive="right")], index=[0, 1, 2, 3, 0], dtype="category", ).cat.as_ordered() @@ -706,16 +711,16 @@ def test_cut_with_nonexact_categorical_indices(): index = pd.CategoricalIndex( [ - Interval(-0.099, 9.9, closed="right"), - Interval(9.9, 19.8, closed="right"), - Interval(19.8, 29.7, closed="right"), - Interval(29.7, 39.6, closed="right"), - Interval(39.6, 49.5, closed="right"), - Interval(49.5, 59.4, closed="right"), - Interval(59.4, 69.3, closed="right"), - Interval(69.3, 79.2, closed="right"), - Interval(79.2, 89.1, closed="right"), - Interval(89.1, 99, closed="right"), + Interval(-0.099, 9.9, inclusive="right"), + Interval(9.9, 19.8, inclusive="right"), + Interval(19.8, 29.7, inclusive="right"), + Interval(29.7, 39.6, inclusive="right"), + Interval(39.6, 49.5, inclusive="right"), + Interval(49.5, 59.4, inclusive="right"), + Interval(59.4, 69.3, inclusive="right"), + Interval(69.3, 79.2, inclusive="right"), + Interval(79.2, 89.1, inclusive="right"), + Interval(89.1, 99, inclusive="right"), ], ordered=True, ) diff --git a/pandas/tests/reshape/test_pivot.py b/pandas/tests/reshape/test_pivot.py index 3950999c5d4fc..8312e3b9de9a7 100644 --- a/pandas/tests/reshape/test_pivot.py +++ b/pandas/tests/reshape/test_pivot.py @@ -301,7 +301,7 @@ def test_pivot_with_interval_index(self, interval_values, dropna): def test_pivot_with_interval_index_margins(self): # GH 25815 - ordered_cat = pd.IntervalIndex.from_arrays([0, 0, 1, 1], [1, 1, 2, 2]) + ordered_cat = pd.IntervalIndex.from_arrays([0, 0, 1, 1], [1, 1, 2, 2], "right") df = DataFrame( { "A": np.arange(4, 0, -1, dtype=np.intp), @@ -319,7 +319,10 @@ def test_pivot_with_interval_index_margins(self): result = pivot_tab["All"] expected = Series( [3, 7, 10], - index=Index([pd.Interval(0, 1), pd.Interval(1, 2), "All"], name="C"), + index=Index( + [pd.Interval(0, 1, "right"), pd.Interval(1, 2, "right"), "All"], + name="C", + ), name="All", dtype=np.intp, ) diff --git a/pandas/tests/reshape/test_qcut.py b/pandas/tests/reshape/test_qcut.py index f7c7204d02a49..0f82bb736c069 100644 --- a/pandas/tests/reshape/test_qcut.py +++ b/pandas/tests/reshape/test_qcut.py @@ -76,7 +76,8 @@ def test_qcut_include_lowest(): Interval(2.25, 4.5), Interval(4.5, 6.75), Interval(6.75, 9), - ] + ], + "right", ) tm.assert_index_equal(ii.categories, ex_levels) @@ -91,7 +92,7 @@ def test_qcut_nas(): def test_qcut_index(): result = qcut([0, 2], 2) - intervals = [Interval(-0.001, 1), Interval(1, 2)] + intervals = [Interval(-0.001, 1, "right"), Interval(1, 2, "right")] expected = Categorical(intervals, ordered=True) tm.assert_categorical_equal(result, expected) @@ -127,7 +128,11 @@ def test_qcut_return_intervals(): res = qcut(ser, [0, 0.333, 0.666, 1]) exp_levels = np.array( - [Interval(-0.001, 2.664), Interval(2.664, 5.328), Interval(5.328, 8)] + [ + Interval(-0.001, 2.664, "right"), + Interval(2.664, 5.328, "right"), + Interval(5.328, 8, "right"), + ] ) exp = Series(exp_levels.take([0, 0, 0, 1, 1, 1, 2, 2, 2])).astype(CDT(ordered=True)) tm.assert_series_equal(res, exp) @@ -183,7 +188,7 @@ def test_qcut_duplicates_bin(kwargs, msg): qcut(values, 3, **kwargs) else: result = qcut(values, 3, **kwargs) - expected = IntervalIndex([Interval(-0.001, 1), Interval(1, 3)]) + expected = IntervalIndex([Interval(-0.001, 1), Interval(1, 3)], "right") tm.assert_index_equal(result.categories, expected) @@ -198,7 +203,7 @@ def test_single_quantile(data, start, end, length, labels): result = qcut(ser, 1, labels=labels) if labels is None: - intervals = IntervalIndex([Interval(start, end)] * length, closed="right") + intervals = IntervalIndex([Interval(start, end)] * length, inclusive="right") expected = Series(intervals).astype(CDT(ordered=True)) else: expected = Series([0] * length, dtype=np.intp) @@ -217,7 +222,7 @@ def test_single_quantile(data, start, end, length, labels): def test_qcut_nat(ser): # see gh-19768 intervals = IntervalIndex.from_tuples( - [(ser[0] - Nano(), ser[2] - Day()), np.nan, (ser[2] - Day(), ser[2])] + [(ser[0] - Nano(), ser[2] - Day()), np.nan, (ser[2] - Day(), ser[2])], "right" ) expected = Series(Categorical(intervals, ordered=True)) @@ -247,7 +252,8 @@ def test_datetime_tz_qcut(bins): Timestamp("2013-01-02 08:00:00", tz=tz), Timestamp("2013-01-03 00:00:00", tz=tz), ), - ] + ], + "right", ) ).astype(CDT(ordered=True)) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/scalar/interval/test_interval.py b/pandas/tests/scalar/interval/test_interval.py index 1f76a7df1e996..878b5e6ec0167 100644 --- a/pandas/tests/scalar/interval/test_interval.py +++ b/pandas/tests/scalar/interval/test_interval.py @@ -13,22 +13,22 @@ @pytest.fixture def interval(): - return Interval(0, 1) + return Interval(0, 1, "right") class TestInterval: def test_properties(self, interval): - assert interval.closed == "right" + assert interval.inclusive == "right" assert interval.left == 0 assert interval.right == 1 assert interval.mid == 0.5 def test_repr(self, interval): - assert repr(interval) == "Interval(0, 1, closed='right')" + assert repr(interval) == "Interval(0, 1, inclusive='right')" assert str(interval) == "(0, 1]" - interval_left = Interval(0, 1, closed="left") - assert repr(interval_left) == "Interval(0, 1, closed='left')" + interval_left = Interval(0, 1, "left") + assert repr(interval_left) == "Interval(0, 1, inclusive='left')" assert str(interval_left) == "[0, 1)" def test_contains(self, interval): @@ -40,18 +40,18 @@ def test_contains(self, interval): with pytest.raises(TypeError, match=msg): interval in interval - interval_both = Interval(0, 1, closed="both") + interval_both = Interval(0, 1, "both") assert 0 in interval_both assert 1 in interval_both - interval_neither = Interval(0, 1, closed="neither") + interval_neither = Interval(0, 1, "neither") assert 0 not in interval_neither assert 0.5 in interval_neither assert 1 not in interval_neither def test_equal(self): - assert Interval(0, 1) == Interval(0, 1, closed="right") - assert Interval(0, 1) != Interval(0, 1, closed="left") + assert Interval(0, 1, "right") == Interval(0, 1, "right") + assert Interval(0, 1, "right") != Interval(0, 1, "left") assert Interval(0, 1) != 0 def test_comparison(self): @@ -129,7 +129,7 @@ def test_is_empty(self, left, right, closed): iv = Interval(left, right, closed) assert iv.is_empty is False - # same endpoint is empty except when closed='both' (contains one point) + # same endpoint is empty except when inclusive='both' (contains one point) iv = Interval(left, left, closed) result = iv.is_empty expected = closed != "both" @@ -152,8 +152,8 @@ def test_construct_errors(self, left, right): Interval(left, right) def test_math_add(self, closed): - interval = Interval(0, 1, closed=closed) - expected = Interval(1, 2, closed=closed) + interval = Interval(0, 1, closed) + expected = Interval(1, 2, closed) result = interval + 1 assert result == expected @@ -173,8 +173,8 @@ def test_math_add(self, closed): interval + "foo" def test_math_sub(self, closed): - interval = Interval(0, 1, closed=closed) - expected = Interval(-1, 0, closed=closed) + interval = Interval(0, 1, closed) + expected = Interval(-1, 0, closed) result = interval - 1 assert result == expected @@ -191,8 +191,8 @@ def test_math_sub(self, closed): interval - "foo" def test_math_mult(self, closed): - interval = Interval(0, 1, closed=closed) - expected = Interval(0, 2, closed=closed) + interval = Interval(0, 1, closed) + expected = Interval(0, 2, closed) result = interval * 2 assert result == expected @@ -213,8 +213,8 @@ def test_math_mult(self, closed): interval * "foo" def test_math_div(self, closed): - interval = Interval(0, 1, closed=closed) - expected = Interval(0, 0.5, closed=closed) + interval = Interval(0, 1, closed) + expected = Interval(0, 0.5, closed) result = interval / 2.0 assert result == expected @@ -231,8 +231,8 @@ def test_math_div(self, closed): interval / "foo" def test_math_floordiv(self, closed): - interval = Interval(1, 2, closed=closed) - expected = Interval(0, 1, closed=closed) + interval = Interval(1, 2, closed) + expected = Interval(0, 1, closed) result = interval // 2 assert result == expected @@ -249,9 +249,9 @@ def test_math_floordiv(self, closed): interval // "foo" def test_constructor_errors(self): - msg = "invalid option for 'closed': foo" + msg = "invalid option for 'inclusive': foo" with pytest.raises(ValueError, match=msg): - Interval(0, 1, closed="foo") + Interval(0, 1, "foo") msg = "left side of interval must be <= right side" with pytest.raises(ValueError, match=msg): diff --git a/pandas/tests/series/indexing/test_setitem.py b/pandas/tests/series/indexing/test_setitem.py index e42039a86fc16..e2a5517066ad9 100644 --- a/pandas/tests/series/indexing/test_setitem.py +++ b/pandas/tests/series/indexing/test_setitem.py @@ -781,7 +781,12 @@ def test_index_putmask(self, obj, key, expected, val): # cast to IntervalDtype[float] Series(interval_range(1, 5, inclusive="right")), Series( - [Interval(1, 2), np.nan, Interval(3, 4), Interval(4, 5)], + [ + Interval(1, 2, "right"), + np.nan, + Interval(3, 4, "right"), + Interval(4, 5, "right"), + ], dtype="interval[float64]", ), 1, @@ -1052,9 +1057,9 @@ class TestSetitemFloatIntervalWithIntIntervalValues(SetitemCastingEquivalents): def test_setitem_example(self): # Just a case here to make obvious what this test class is aimed at - idx = IntervalIndex.from_breaks(range(4)) + idx = IntervalIndex.from_breaks(range(4), inclusive="right") obj = Series(idx) - val = Interval(0.5, 1.5) + val = Interval(0.5, 1.5, "right") obj[0] = val assert obj.dtype == "Interval[float64, right]" @@ -1348,7 +1353,7 @@ def obj(self): @pytest.mark.parametrize( - "val", ["foo", Period("2016", freq="Y"), Interval(1, 2, closed="both")] + "val", ["foo", Period("2016", freq="Y"), Interval(1, 2, inclusive="both")] ) @pytest.mark.parametrize("exp_dtype", [object]) class TestPeriodIntervalCoercion(CoercionTest): @@ -1547,7 +1552,7 @@ def test_setitem_int_as_positional_fallback_deprecation(): # Once the deprecation is enforced, we will have # expected = Series([1, 2, 3, 4, 5], index=[1.1, 2.1, 3.0, 4.1, 5.0]) - ii = IntervalIndex.from_breaks(range(10))[::2] + ii = IntervalIndex.from_breaks(range(10), inclusive="right")[::2] ser2 = Series(range(len(ii)), index=ii) expected2 = ser2.copy() expected2.iloc[-1] = 9 diff --git a/pandas/tests/series/test_constructors.py b/pandas/tests/series/test_constructors.py index e416b1f625993..e0b180bf0c6f4 100644 --- a/pandas/tests/series/test_constructors.py +++ b/pandas/tests/series/test_constructors.py @@ -1172,7 +1172,7 @@ def test_constructor_datetime64_bigendian(self): @pytest.mark.parametrize("interval_constructor", [IntervalIndex, IntervalArray]) def test_construction_interval(self, interval_constructor): # construction from interval & array of intervals - intervals = interval_constructor.from_breaks(np.arange(3), closed="right") + intervals = interval_constructor.from_breaks(np.arange(3), inclusive="right") result = Series(intervals) assert result.dtype == "interval[int64, right]" tm.assert_index_equal(Index(result.values), Index(intervals)) @@ -1182,7 +1182,7 @@ def test_construction_interval(self, interval_constructor): ) def test_constructor_infer_interval(self, data_constructor): # GH 23563: consistent closed results in interval dtype - data = [Interval(0, 1), Interval(0, 2), None] + data = [Interval(0, 1, "right"), Interval(0, 2, "right"), None] result = Series(data_constructor(data)) expected = Series(IntervalArray(data)) assert result.dtype == "interval[float64, right]" @@ -1193,7 +1193,7 @@ def test_constructor_infer_interval(self, data_constructor): ) def test_constructor_interval_mixed_closed(self, data_constructor): # GH 23563: mixed closed results in object dtype (not interval dtype) - data = [Interval(0, 1, closed="both"), Interval(0, 2, closed="neither")] + data = [Interval(0, 1, inclusive="both"), Interval(0, 2, inclusive="neither")] result = Series(data_constructor(data)) assert result.dtype == object assert result.tolist() == data diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index 2d73b8e91e831..85a240a3e825d 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -1101,19 +1101,26 @@ def test_value_counts(self): # assert isinstance(factor, n) result = algos.value_counts(factor) breaks = [-1.194, -0.535, 0.121, 0.777, 1.433] - index = IntervalIndex.from_breaks(breaks).astype(CDT(ordered=True)) + index = IntervalIndex.from_breaks(breaks, inclusive="right").astype( + CDT(ordered=True) + ) expected = Series([1, 1, 1, 1], index=index) tm.assert_series_equal(result.sort_index(), expected.sort_index()) def test_value_counts_bins(self): s = [1, 2, 3, 4] result = algos.value_counts(s, bins=1) - expected = Series([4], index=IntervalIndex.from_tuples([(0.996, 4.0)])) + expected = Series( + [4], index=IntervalIndex.from_tuples([(0.996, 4.0)], inclusive="right") + ) tm.assert_series_equal(result, expected) result = algos.value_counts(s, bins=2, sort=False) expected = Series( - [2, 2], index=IntervalIndex.from_tuples([(0.996, 2.5), (2.5, 4.0)]) + [2, 2], + index=IntervalIndex.from_tuples( + [(0.996, 2.5), (2.5, 4.0)], inclusive="right" + ), ) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/util/test_assert_frame_equal.py b/pandas/tests/util/test_assert_frame_equal.py index 6ff1a1c17b179..c3c5f2fdc9d29 100644 --- a/pandas/tests/util/test_assert_frame_equal.py +++ b/pandas/tests/util/test_assert_frame_equal.py @@ -247,7 +247,7 @@ def test_assert_frame_equal_extension_dtype_mismatch(): def test_assert_frame_equal_interval_dtype_mismatch(): # https://github.com/pandas-dev/pandas/issues/32747 - left = DataFrame({"a": [pd.Interval(0, 1)]}, dtype="interval") + left = DataFrame({"a": [pd.Interval(0, 1, "right")]}, dtype="interval") right = left.astype(object) msg = ( diff --git a/pandas/tests/util/test_assert_interval_array_equal.py b/pandas/tests/util/test_assert_interval_array_equal.py index 243f357d7298c..29ebc00b2e69a 100644 --- a/pandas/tests/util/test_assert_interval_array_equal.py +++ b/pandas/tests/util/test_assert_interval_array_equal.py @@ -25,7 +25,7 @@ def test_interval_array_equal_closed_mismatch(): msg = """\ IntervalArray are different -Attribute "closed" are different +Attribute "inclusive" are different \\[left\\]: left \\[right\\]: right""" diff --git a/pandas/tests/util/test_assert_series_equal.py b/pandas/tests/util/test_assert_series_equal.py index 93a2e4b83e760..dcf1fe291f179 100644 --- a/pandas/tests/util/test_assert_series_equal.py +++ b/pandas/tests/util/test_assert_series_equal.py @@ -256,7 +256,7 @@ def test_assert_series_equal_extension_dtype_mismatch(): def test_assert_series_equal_interval_dtype_mismatch(): # https://github.com/pandas-dev/pandas/issues/32747 - left = Series([pd.Interval(0, 1)], dtype="interval") + left = Series([pd.Interval(0, 1, "right")], dtype="interval") right = left.astype(object) msg = """Attributes of Series are different