diff --git a/pandas/_libs/interval.pyi b/pandas/_libs/interval.pyi index ba0a339fa93dd..bad0f2bab93d8 100644 --- a/pandas/_libs/interval.pyi +++ b/pandas/_libs/interval.pyi @@ -17,7 +17,7 @@ from pandas._typing import ( Timestamp, ) -VALID_CLOSED: frozenset[str] +VALID_INCLUSIVE: frozenset[str] _OrderableScalarT = TypeVar("_OrderableScalarT", int, float) _OrderableTimesT = TypeVar("_OrderableTimesT", Timestamp, Timedelta) @@ -52,7 +52,9 @@ class IntervalMixin: def open_right(self) -> bool: ... @property def is_empty(self) -> bool: ... - def _check_closed_matches(self, other: IntervalMixin, name: str = ...) -> None: ... + def _check_inclusive_matches( + self, other: IntervalMixin, name: str = ... + ) -> None: ... def _warning_interval( inclusive, closed @@ -150,7 +152,7 @@ class Interval(IntervalMixin, Generic[_OrderableT]): def overlaps(self: Interval[_OrderableT], other: Interval[_OrderableT]) -> bool: ... def intervals_to_interval_bounds( - intervals: np.ndarray, validate_closed: bool = ... + intervals: np.ndarray, validate_inclusive: bool = ... ) -> tuple[np.ndarray, np.ndarray, IntervalInclusiveType]: ... class IntervalTree(IntervalMixin): diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 79b3c0d056735..bc0a63c5c5a33 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -56,7 +56,7 @@ from pandas._libs.tslibs.util cimport ( is_timedelta64_object, ) -VALID_CLOSED = frozenset(['both', 'neither', 'left', 'right']) +VALID_INCLUSIVE = frozenset(['both', 'neither', 'left', 'right']) cdef class IntervalMixin: @@ -85,7 +85,7 @@ cdef class IntervalMixin: Returns ------- bool - True if the Interval is closed on the left-side. + True if the Interval is closed on the right-side. """ return self.inclusive in ('right', 'both') @@ -99,7 +99,7 @@ cdef class IntervalMixin: Returns ------- bool - True if the Interval is closed on the left-side. + True if the Interval is not closed on the left-side. """ return not self.closed_left @@ -113,7 +113,7 @@ cdef class IntervalMixin: Returns ------- bool - True if the Interval is closed on the left-side. + True if the Interval is not closed on the right-side. """ return not self.closed_right @@ -188,7 +188,7 @@ cdef class IntervalMixin: """ return (self.right == self.left) & (self.inclusive != 'both') - def _check_closed_matches(self, other, name='other'): + def _check_inclusive_matches(self, other, name='other'): """ Check if the inclusive attribute of `other` matches. @@ -203,7 +203,7 @@ cdef class IntervalMixin: Raises ------ ValueError - When `other` is not closed exactly the same as self. + When `other` is not inclusive exactly the same as self. """ if self.inclusive != other.inclusive: raise ValueError(f"'{name}.inclusive' is {repr(other.inclusive)}, " @@ -259,14 +259,14 @@ cdef class Interval(IntervalMixin): .. deprecated:: 1.5.0 inclusive : {'both', 'neither', 'left', 'right'}, default 'both' - Whether the interval is closed on the left-side, right-side, both or + Whether the interval is inclusive 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 + IntervalIndex : An Index of Interval objects that are all inclusive on the same side. cut : Convert continuous data into discrete bins (Categorical of Interval objects). @@ -279,13 +279,13 @@ cdef class Interval(IntervalMixin): The parameters `left` and `right` must be from the same type, you must be able to compare them and they must satisfy ``left <= right``. - A closed interval (in mathematics denoted by square brackets) contains - its endpoints, i.e. the closed interval ``[0, 5]`` is characterized by the + A inclusive interval (in mathematics denoted by square brackets) contains + its endpoints, i.e. the inclusive interval ``[0, 5]`` is characterized by the 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 ``inclusive='neither'`` stands for. - Intervals can also be half-open or half-closed, i.e. ``[0, 5)`` is + Intervals can also be half-open or half-inclusive, i.e. ``[0, 5)`` is described by ``0 <= x < 5`` (``inclusive='left'``) and ``(0, 5]`` is described by ``0 < x <= 5`` (``inclusive='right'``). @@ -352,7 +352,7 @@ cdef class Interval(IntervalMixin): cdef readonly str inclusive """ - Whether the interval is closed on the left-side, right-side, both or + Whether the interval is inclusive on the left-side, right-side, both or neither. """ @@ -368,7 +368,7 @@ cdef class Interval(IntervalMixin): if inclusive is None: inclusive = "right" - if inclusive not in VALID_CLOSED: + if inclusive not in VALID_INCLUSIVE: raise ValueError(f"invalid option for 'inclusive': {inclusive}") if not left <= right: raise ValueError("left side of interval must be <= right side") @@ -522,7 +522,7 @@ cdef class Interval(IntervalMixin): """ Check whether two Interval objects overlap. - 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. @@ -551,7 +551,7 @@ cdef class Interval(IntervalMixin): >>> i1.overlaps(i3) False - Intervals that share closed endpoints overlap: + Intervals that share inclusive endpoints overlap: >>> i4 = pd.Interval(0, 1, inclusive='both') >>> i5 = pd.Interval(1, 2, inclusive='both') @@ -568,7 +568,7 @@ cdef class Interval(IntervalMixin): raise TypeError("`other` must be an Interval, " f"got {type(other).__name__}") - # equality is okay if both endpoints are closed (overlap at a point) + # equality is okay if both endpoints are inclusive (overlap at a point) op1 = le if (self.closed_left and other.closed_right) else lt op2 = le if (other.closed_left and self.closed_right) else lt @@ -580,16 +580,16 @@ cdef class Interval(IntervalMixin): @cython.wraparound(False) @cython.boundscheck(False) -def intervals_to_interval_bounds(ndarray intervals, bint validate_closed=True): +def intervals_to_interval_bounds(ndarray intervals, bint validate_inclusive=True): """ Parameters ---------- intervals : ndarray Object array of Intervals / nulls. - validate_closed: bool, default True - Boolean indicating if all intervals must be closed on the same side. - Mismatching closed will raise if True, else return None for closed. + validate_inclusive: bool, default True + Boolean indicating if all intervals must be inclusive on the same side. + Mismatching inclusive will raise if True, else return None for inclusive. Returns ------- @@ -602,7 +602,7 @@ def intervals_to_interval_bounds(ndarray intervals, bint validate_closed=True): object inclusive = None, interval Py_ssize_t i, n = len(intervals) ndarray left, right - bint seen_closed = False + bint seen_inclusive = False left = np.empty(n, dtype=intervals.dtype) right = np.empty(n, dtype=intervals.dtype) @@ -620,13 +620,13 @@ def intervals_to_interval_bounds(ndarray intervals, bint validate_closed=True): left[i] = interval.left right[i] = interval.right - if not seen_closed: - seen_closed = True + if not seen_inclusive: + seen_inclusive = True inclusive = interval.inclusive elif inclusive != interval.inclusive: inclusive = None - if validate_closed: - raise ValueError("intervals must all be closed on the same side") + if validate_inclusive: + raise ValueError("intervals must all be inclusive on the same side") return left, right, inclusive diff --git a/pandas/core/arrays/arrow/_arrow_utils.py b/pandas/core/arrays/arrow/_arrow_utils.py index 5ed10661e8983..79b79a8ae8ff1 100644 --- a/pandas/core/arrays/arrow/_arrow_utils.py +++ b/pandas/core/arrays/arrow/_arrow_utils.py @@ -11,7 +11,7 @@ from pandas.util._decorators import deprecate_kwarg from pandas.util._exceptions import find_stack_level -from pandas.core.arrays.interval import VALID_CLOSED +from pandas.core.arrays.interval import VALID_INCLUSIVE def fallback_performancewarning(version: str | None = None) -> None: @@ -111,8 +111,8 @@ class ArrowIntervalType(pyarrow.ExtensionType): def __init__(self, subtype, inclusive: IntervalInclusiveType) -> None: # attributes need to be set first before calling # super init (as that calls serialize) - assert inclusive in VALID_CLOSED - self._closed: IntervalInclusiveType = inclusive + assert inclusive in VALID_INCLUSIVE + self._inclusive: IntervalInclusiveType = inclusive if not isinstance(subtype, pyarrow.DataType): subtype = pyarrow.type_for_alias(str(subtype)) self._subtype = subtype @@ -126,7 +126,7 @@ def subtype(self): @property def inclusive(self) -> IntervalInclusiveType: - return self._closed + return self._inclusive @property def closed(self) -> IntervalInclusiveType: @@ -135,7 +135,7 @@ def closed(self) -> IntervalInclusiveType: FutureWarning, stacklevel=find_stack_level(), ) - return self._closed + return self._inclusive def __arrow_ext_serialize__(self) -> bytes: metadata = {"subtype": str(self.subtype), "inclusive": self.inclusive} diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index ea0e7a769c25e..6469dccf6e2d5 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -23,7 +23,7 @@ from pandas._libs import lib from pandas._libs.interval import ( - VALID_CLOSED, + VALID_INCLUSIVE, Interval, IntervalMixin, intervals_to_interval_bounds, @@ -130,7 +130,7 @@ Array-like containing Interval objects from which to build the %(klass)s. inclusive : {'left', 'right', 'both', 'neither'}, default 'right' - Whether the intervals are closed on the left-side, right-side, both or + Whether the intervals are inclusive on the left-side, right-side, both or neither. dtype : dtype or None, default None If None, dtype will be inferred. @@ -185,7 +185,8 @@ _interval_shared_docs["class"] % { "klass": "IntervalArray", - "summary": "Pandas array for interval data that are closed on the same side.", + "summary": "Pandas array for interval data that are inclusive on the same " + "side.", "versionadded": "0.24.0", "name": "", "extra_attributes": "", @@ -254,13 +255,13 @@ 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=inclusive is None + left, right, infer_inclusive = intervals_to_interval_bounds( + data, validate_inclusive=inclusive is None ) if left.dtype == object: left = lib.maybe_convert_objects(left) right = lib.maybe_convert_objects(right) - inclusive = inclusive or infer_closed + inclusive = inclusive or infer_inclusive return cls._simple_new( left, @@ -389,7 +390,7 @@ def _from_factorized( breaks : array-like (1-dimensional) Left and right bounds for each interval. inclusive : {'left', 'right', 'both', 'neither'}, default 'right' - Whether the intervals are closed on the left-side, right-side, both + Whether the intervals are inclusive on the left-side, right-side, both or neither. copy : bool, default False Copy the data. @@ -455,7 +456,7 @@ def from_breaks( right : array-like (1-dimensional) Right bounds for each interval. inclusive : {'left', 'right', 'both', 'neither'}, default 'right' - Whether the intervals are closed on the left-side, right-side, both + Whether the intervals are inclusive on the left-side, right-side, both or neither. copy : bool, default False Copy the data. @@ -542,7 +543,7 @@ def from_arrays( data : array-like (1-dimensional) Array of tuples. inclusive : {'left', 'right', 'both', 'neither'}, default 'right' - Whether the intervals are closed on the left-side, right-side, both + Whether the intervals are inclusive on the left-side, right-side, both or neither. copy : bool, default False By-default copy the data, this is compat only and ignored. @@ -629,7 +630,7 @@ def _validate(self): * left and right have the same missing values * left is always below right """ - if self.inclusive not in VALID_CLOSED: + if self.inclusive not in VALID_INCLUSIVE: msg = f"invalid option for 'inclusive': {self.inclusive}" raise ValueError(msg) if len(self._left) != len(self._right): @@ -745,7 +746,7 @@ def _cmp_method(self, other, op): # for categorical defer to categories for dtype other_dtype = other.categories.dtype - # extract intervals if we have interval categories with matching closed + # extract intervals if we have interval categories with matching inclusive if is_interval_dtype(other_dtype): if self.inclusive != other.categories.inclusive: return invalid_comparison(self, other, op) @@ -754,7 +755,7 @@ def _cmp_method(self, other, op): other.codes, allow_fill=True, fill_value=other.categories._na_value ) - # interval-like -> need same closed and matching endpoints + # interval-like -> need same inclusive and matching endpoints if is_interval_dtype(other_dtype): if self.inclusive != other.inclusive: return invalid_comparison(self, other, op) @@ -994,7 +995,7 @@ def _concat_same_type( """ 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.") + raise ValueError("Intervals must all be inclusive on the same side.") inclusive = inclusive_set.pop() left = np.concatenate([interval.left for interval in to_concat]) @@ -1120,7 +1121,7 @@ def _validate_listlike(self, value): # list-like of intervals try: array = IntervalArray(value) - self._check_closed_matches(array, name="value") + self._check_inclusive_matches(array, name="value") value_left, value_right = array.left, array.right except TypeError as err: # wrong type: not interval or NA @@ -1140,7 +1141,7 @@ def _validate_listlike(self, value): def _validate_scalar(self, value): if isinstance(value, Interval): - self._check_closed_matches(value, name="value") + self._check_inclusive_matches(value, name="value") left, right = value.left, value.right # TODO: check subdtype match like _validate_setitem_value? elif is_valid_na_for_dtype(value, self.left.dtype): @@ -1166,7 +1167,7 @@ def _validate_setitem_value(self, value): elif isinstance(value, Interval): # scalar - self._check_closed_matches(value, name="value") + self._check_inclusive_matches(value, name="value") value_left, value_right = value.left, value.right self.left._validate_fill_value(value_left) self.left._validate_fill_value(value_right) @@ -1352,7 +1353,7 @@ def overlaps(self, other): msg = f"`other` must be Interval-like, got {type(other).__name__}" raise TypeError(msg) - # equality is okay if both endpoints are closed (overlap at a point) + # equality is okay if both endpoints are inclusive (overlap at a point) op1 = le if (self.closed_left and other.closed_right) else lt op2 = le if (other.closed_left and self.closed_right) else lt @@ -1366,7 +1367,7 @@ def overlaps(self, other): @property def inclusive(self) -> IntervalInclusiveType: """ - Whether the intervals are closed on the left-side, right-side, both or + Whether the intervals are inclusive on the left-side, right-side, both or neither. """ return self.dtype.inclusive @@ -1482,7 +1483,7 @@ def set_closed( def set_inclusive( self: IntervalArrayT, inclusive: IntervalInclusiveType ) -> IntervalArrayT: - if inclusive not in VALID_CLOSED: + if inclusive not in VALID_INCLUSIVE: msg = f"invalid option for 'inclusive': {inclusive}" raise ValueError(msg) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 78096d836f5b0..9683c1dd93645 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1124,7 +1124,7 @@ def __new__( # generally for pickle compat u = object.__new__(cls) u._subtype = None - u._closed = inclusive + u._inclusive = inclusive return u elif isinstance(subtype, str) and subtype.lower() == "interval": subtype = None @@ -1166,7 +1166,7 @@ def __new__( except KeyError: u = object.__new__(cls) u._subtype = subtype - u._closed = inclusive + u._inclusive = inclusive cls._cache_dtypes[key] = u return u @@ -1184,7 +1184,7 @@ def _can_hold_na(self) -> bool: @property def inclusive(self): - return self._closed + return self._inclusive @property def closed(self): @@ -1193,7 +1193,7 @@ def closed(self): FutureWarning, stacklevel=find_stack_level(), ) - return self._closed + return self._inclusive @property def subtype(self): @@ -1274,7 +1274,7 @@ def __setstate__(self, state) -> None: # pickle -> need to set the settable private ones here (see GH26067) self._subtype = state["subtype"] # backward-compat older pickles won't have "inclusive" key - self._closed = state.pop("inclusive", None) + self._inclusive = state.pop("inclusive", None) @classmethod def is_dtype(cls, dtype: object) -> bool: diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index ced675fe9a3cf..23f2e724e208c 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -153,7 +153,7 @@ def _new_IntervalIndex(cls, d): _interval_shared_docs["class"] % { "klass": "IntervalIndex", - "summary": "Immutable index of intervals that are closed on the same side.", + "summary": "Immutable index of intervals that are inclusive on the same side.", "name": _index_doc_kwargs["name"], "versionadded": "0.20.0", "extra_attributes": "is_overlapping\nvalues\n", @@ -473,7 +473,7 @@ def is_overlapping(self) -> bool: >>> index.is_overlapping True - Intervals that share closed endpoints overlap: + Intervals that share inclusive endpoints overlap: >>> index = pd.interval_range(0, 3, inclusive='both') >>> index @@ -1009,7 +1009,7 @@ def interval_range( name : str, default None Name of the resulting IntervalIndex. inclusive : {"both", "neither", "left", "right"}, default "both" - Include boundaries; Whether to set each bound as closed or open. + Include boundaries; Whether to set each bound as inclusive or not. .. versionadded:: 1.5.0 closed : {'left', 'right', 'both', 'neither'}, default 'right' @@ -1026,7 +1026,7 @@ def interval_range( See Also -------- - IntervalIndex : An Index of intervals that are all closed on the same side. + IntervalIndex : An Index of intervals that are all inclusive on the same side. Notes ----- @@ -1079,7 +1079,7 @@ def interval_range( dtype='interval[float64, right]') The ``inclusive`` parameter specifies which endpoints of the individual - intervals within the ``IntervalIndex`` are closed. + intervals within the ``IntervalIndex`` are inclusive. >>> pd.interval_range(end=5, periods=4, inclusive='both') IntervalIndex([[1, 2], [2, 3], [3, 4], [4, 5]], diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index e127fe27b6209..695b06690b358 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -826,7 +826,7 @@ def test_unpickling_without_closed(self): # GH#38394 dtype = IntervalDtype("interval") - assert dtype._closed is None + assert dtype._inclusive is None tm.round_trip_pickle(dtype) diff --git a/pandas/tests/indexes/interval/test_constructors.py b/pandas/tests/indexes/interval/test_constructors.py index 1966f344356a3..8c8998a8e4be9 100644 --- a/pandas/tests/indexes/interval/test_constructors.py +++ b/pandas/tests/indexes/interval/test_constructors.py @@ -401,7 +401,7 @@ def test_constructor_string(self): def test_constructor_errors(self, constructor): # mismatched closed within intervals with no constructor override ivs = [Interval(0, 1, inclusive="right"), Interval(2, 3, inclusive="left")] - msg = "intervals must all be closed on the same side" + msg = "intervals must all be inclusive on the same side" with pytest.raises(ValueError, match=msg): constructor(ivs)