From 538ba43ffec73c789d39f7619e12a7ae40159525 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sat, 26 Mar 2022 23:53:01 +0800 Subject: [PATCH 01/60] Update interval.pyi --- pandas/_libs/interval.pyi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/interval.pyi b/pandas/_libs/interval.pyi index bad6013aa58b6..66031ee91109c 100644 --- a/pandas/_libs/interval.pyi +++ b/pandas/_libs/interval.pyi @@ -60,14 +60,14 @@ class Interval(IntervalMixin, Generic[_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, - closed: IntervalClosedType = ..., + inclusive: IntervalClosedType = ..., ): ... def __hash__(self) -> int: ... @overload @@ -155,7 +155,7 @@ class IntervalTree(IntervalMixin): self, left: np.ndarray, right: np.ndarray, - closed: IntervalClosedType = ..., + inclusive: IntervalClosedType = ..., leaf_size: int = ..., ): ... @property From a9da7bd16cae0f4e6a9fb01ce60a31e1d187c9ba Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sun, 27 Mar 2022 00:01:01 +0800 Subject: [PATCH 02/60] Update interval.pyx --- pandas/_libs/interval.pyx | 102 +++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 4c8419b78e2b8..2d5d93de53934 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -69,7 +69,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 +83,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,39 +150,39 @@ 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'): """ @@ -201,15 +201,15 @@ 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')) cdef class Interval(IntervalMixin): @@ -222,7 +222,7 @@ cdef class Interval(IntervalMixin): Left bound for the interval. right : orderable scalar Right bound for the interval. - closed : {'right', 'left', 'both', 'neither'}, default 'right' + inclusive : {'right', 'left', 'both', 'neither'}, default 'right' Whether the interval is closed on the left-side, right-side, both or neither. See the Notes for more detailed explanation. @@ -243,13 +243,13 @@ 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 -------- @@ -257,14 +257,14 @@ cdef class Interval(IntervalMixin): >>> iv = pd.Interval(left=0, right=5) >>> 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 +284,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 +312,21 @@ 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, str inclusive='right'): # 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}") + 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 +336,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 +346,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 +356,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 +368,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 +386,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 +402,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 ( isinstance(y, Interval) and ( @@ -411,7 +411,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 __sub__(self, y): @@ -420,25 +420,25 @@ 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): - 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 __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): @@ -476,14 +476,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 """ @@ -519,10 +519,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 @@ -545,13 +545,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" From b222280ed3a276902450f0a0977f2175e1fd1867 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sun, 27 Mar 2022 00:03:49 +0800 Subject: [PATCH 03/60] Update intervaltree.pxi.in --- pandas/_libs/intervaltree.pxi.in | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pandas/_libs/intervaltree.pxi.in b/pandas/_libs/intervaltree.pxi.in index 547fcc0b8aa07..dcbef3cc85cb2 100644 --- a/pandas/_libs/intervaltree.pxi.in +++ b/pandas/_libs/intervaltree.pxi.in @@ -34,18 +34,18 @@ 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='right', leaf_size=100): """ Parameters ---------- left, right : np.ndarray[ndim=1] Left and right bounds for each interval. Assumed to contain no NaNs. - closed : {'left', 'right', 'both', 'neither'}, optional + inclusive : {'left', 'right', 'both', 'neither'}, optional Whether the intervals are closed on the left-side, right-side, both or neither. Defaults to 'right'. leaf_size : int, optional @@ -53,8 +53,8 @@ cdef class IntervalTree(IntervalMixin): 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) + 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 +64,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 +73,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 +102,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 +180,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 +251,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 +265,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, From 466f35a53cc4330db9e6d24f0a77fc5d97609677 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sun, 27 Mar 2022 00:05:47 +0800 Subject: [PATCH 04/60] Update lib.pyx --- pandas/_libs/lib.pyx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pandas/_libs/lib.pyx b/pandas/_libs/lib.pyx index 18a58902075f2..ce284579daf3a 100644 --- a/pandas/_libs/lib.pyx +++ b/pandas/_libs/lib.pyx @@ -2140,7 +2140,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 @@ -2153,15 +2153,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: @@ -2184,7 +2184,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 From 323f0d890ba74b83e76836f0a4d8b97f65696875 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sun, 27 Mar 2022 00:06:12 +0800 Subject: [PATCH 05/60] Update asserters.py --- pandas/_testing/asserters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/_testing/asserters.py b/pandas/_testing/asserters.py index dda0d9549e7b3..a0f3e4536fecb 100644 --- a/pandas/_testing/asserters.py +++ b/pandas/_testing/asserters.py @@ -616,7 +616,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"): From c4931c9ea0020b2a6a8b3492f94ffdc701e0746b Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sun, 27 Mar 2022 00:06:37 +0800 Subject: [PATCH 06/60] Update _arrow_utils.py --- pandas/core/arrays/_arrow_utils.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pandas/core/arrays/_arrow_utils.py b/pandas/core/arrays/_arrow_utils.py index dae8e2c394abc..d27dee731221f 100644 --- a/pandas/core/arrays/_arrow_utils.py +++ b/pandas/core/arrays/_arrow_utils.py @@ -88,11 +88,11 @@ def to_pandas_dtype(self): class ArrowIntervalType(pyarrow.ExtensionType): - def __init__(self, subtype, closed) -> None: + def __init__(self, subtype, inclusive) -> None: # attributes need to be set first before calling # super init (as that calls serialize) - assert closed in VALID_CLOSED - self._closed = 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 @@ -105,37 +105,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 From 40bfa7661a6c9d5594df216d0357ec2ea2d3afa2 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sun, 27 Mar 2022 00:07:13 +0800 Subject: [PATCH 07/60] Update interval.py --- pandas/core/arrays/interval.py | 131 ++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 61 deletions(-) diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index e14eec419377c..ef5cb511baa93 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -122,7 +122,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 +137,7 @@ ---------- left right -closed +inclusive mid length is_empty @@ -217,7 +217,7 @@ class IntervalArray(IntervalMixin, ExtensionArray): def __new__( cls: type[IntervalArrayT], data, - closed=None, + inclusive=None, dtype: Dtype | None = None, copy: bool = False, verify_integrity: bool = True, @@ -228,7 +228,7 @@ def __new__( 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 +242,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, copy=copy, dtype=dtype, verify_integrity=verify_integrity, @@ -263,17 +263,17 @@ def _simple_new( cls: type[IntervalArrayT], left, right, - closed=None, + inclusive=None, 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 + if inclusive is None and isinstance(dtype, IntervalDtype): + inclusive = dtype.inclusive - closed = closed or "right" + inclusive = inclusive or "right" left = ensure_index(left, copy=copy) right = ensure_index(right, copy=copy) @@ -289,11 +289,11 @@ def _simple_new( 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 +336,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 +364,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 +374,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 @@ -416,13 +416,15 @@ def _from_factorized( def from_breaks( cls: type[IntervalArrayT], breaks, - closed="right", + inclusive="right", 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 +436,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 @@ -492,7 +494,7 @@ def from_arrays( cls: type[IntervalArrayT], left, right, - closed="right", + inclusive="right", copy: bool = False, dtype: Dtype | None = None, ) -> IntervalArrayT: @@ -500,7 +502,7 @@ 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, copy=copy, dtype=dtype, verify_integrity=True ) _interval_shared_docs["from_tuples"] = textwrap.dedent( @@ -511,7 +513,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 @@ -555,7 +557,7 @@ def from_arrays( def from_tuples( cls: type[IntervalArrayT], data, - closed="right", + inclusive="right", copy: bool = False, dtype: Dtype | None = None, ) -> IntervalArrayT: @@ -582,7 +584,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 +592,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 +626,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 +674,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, self.inclusive) if np.ndim(left) > 1: # GH#30588 multi-dimensional indexer disallowed raise ValueError("multi-dimensional indexing not allowed") @@ -711,7 +715,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 +724,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 +940,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 +960,14 @@ def _concat_same_type( ------- IntervalArray """ - closed = {interval.closed for interval in to_concat} - if len(closed) != 1: + inclusive = {interval.inclusive for interval in to_concat} + if len(inclusive) != 1: raise ValueError("Intervals must all be closed on the same side.") - closed = closed.pop() + inclusive = inclusive.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 +979,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) @@ -1129,7 +1133,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 +1261,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 +1285,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]) """ ) @@ -1328,12 +1332,12 @@ def overlaps(self, other): # --------------------------------------------------------------------- @property - def closed(self): + def inclusive(self): """ 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 +1346,7 @@ def closed(self): 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. @@ -1375,13 +1379,18 @@ def closed(self): ), } ) - 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 +1412,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 +1438,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) return result def __arrow_array__(self, type=None): @@ -1454,7 +1463,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 +1486,12 @@ 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}) attributes" ) else: raise TypeError( @@ -1633,7 +1642,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) From 23ad9c8a70991695210508b8e799e278c314d96f Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sun, 27 Mar 2022 00:07:36 +0800 Subject: [PATCH 08/60] Update cast.py --- pandas/core/dtypes/cast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index cba055d5b4345..6cb9cba18dc8b 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -491,7 +491,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 @@ -810,7 +810,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 From f700477c19a527e6348604bad721ce8a567c0850 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sun, 27 Mar 2022 00:07:59 +0800 Subject: [PATCH 09/60] Update dtypes.py --- pandas/core/dtypes/dtypes.py | 63 ++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 66eed0a75fa19..9312d4fbd1d87 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1038,7 +1038,7 @@ class IntervalDtype(PandasExtensionDtype): Examples -------- - >>> pd.IntervalDtype(subtype='int64', closed='both') + >>> pd.IntervalDtype(subtype='int64', inclusive='both') interval[int64, both] """ @@ -1049,27 +1049,34 @@ 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[^,]+)(, (?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): 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'") + 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: @@ -1077,7 +1084,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 @@ -1087,14 +1094,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) @@ -1109,13 +1116,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 @@ -1132,7 +1139,7 @@ def _can_hold_na(self) -> bool: return True @property - def closed(self): + def inclusive(self): return self._closed @property @@ -1184,10 +1191,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 @@ -1201,7 +1208,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 @@ -1214,8 +1221,8 @@ def __setstate__(self, state): # 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: @@ -1257,14 +1264,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) @@ -1272,8 +1279,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 @@ -1281,7 +1288,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): From 3835d29c835a1219fdfc327adc4eeb3ebd048689 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sun, 27 Mar 2022 00:08:48 +0800 Subject: [PATCH 10/60] Update interval.py --- pandas/core/indexes/interval.py | 44 +++++++++++++++++---------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index c1d7eb972e1f4..a77a47e52a3d7 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -188,12 +188,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 @@ -211,7 +211,7 @@ class IntervalIndex(ExtensionIndex): def __new__( cls, data, - closed=None, + inclusive=None, dtype: Dtype | None = None, copy: bool = False, name: Hashable = None, @@ -223,7 +223,7 @@ def __new__( with rewrite_exception("IntervalArray", cls.__name__): array = IntervalArray( data, - closed=closed, + inclusive=inclusive, copy=copy, dtype=dtype, verify_integrity=verify_integrity, @@ -250,14 +250,14 @@ def __new__( def from_breaks( cls, breaks, - closed: str = "right", + inclusive: str = "right", name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, ) -> IntervalIndex: 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) @@ -281,14 +281,14 @@ def from_arrays( cls, left, right, - closed: str = "right", + inclusive: str = "right", name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, ) -> IntervalIndex: 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) @@ -311,13 +311,15 @@ def from_arrays( def from_tuples( cls, data, - closed: str = "right", + inclusive: str = "right", name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, ) -> IntervalIndex: 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) # -------------------------------------------------------------------- @@ -327,7 +329,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: """ @@ -362,7 +364,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 @@ -417,7 +419,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. @@ -441,9 +443,9 @@ 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, closed='both') + >>> index = pd.interval_range(0, 3, inclusive='both') >>> index IntervalIndex([[0, 1], [1, 2], [2, 3]], dtype='interval[int64, both]') @@ -452,7 +454,7 @@ def is_overlapping(self) -> bool: Intervals that only have an open endpoint in common do not overlap: - >>> index = pd.interval_range(0, 3, closed='left') + >>> index = pd.interval_range(0, 3, inclusive='left') >>> index IntervalIndex([[0, 1), [1, 2), [2, 3)], dtype='interval[int64, left]') @@ -518,7 +520,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: @@ -625,7 +627,7 @@ def get_loc( 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): @@ -686,7 +688,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) @@ -836,7 +838,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: @@ -1118,4 +1120,4 @@ def interval_range( else: breaks = timedelta_range(start=start, end=end, periods=periods, freq=freq) - return IntervalIndex.from_breaks(breaks, name=name, closed=closed) + return IntervalIndex.from_breaks(breaks, name=name, inclusive=closed) From 6ac2f6abcc979d57b99113c7f2ec24a5d8c061d8 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sun, 27 Mar 2022 00:09:26 +0800 Subject: [PATCH 11/60] Update blocks.py --- pandas/core/internals/blocks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 26569b571724d..c1fb8df34e566 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -1959,8 +1959,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 From e53605df673423adda5ca48fcc41e1d9a434dd84 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Sun, 27 Mar 2022 00:09:51 +0800 Subject: [PATCH 12/60] Update tile.py --- pandas/core/reshape/tile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index d8c4f3f3da765..c2031cf8bf3d1 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -560,7 +560,7 @@ def _format_labels( bins, precision: int, right: bool = True, include_lowest: bool = False, dtype=None ): """based on the dtype, return our labels""" - closed = "right" if right else "left" + inclusive = "right" if right else "left" formatter: Callable[[Any], Timestamp] | Callable[[Any], Timedelta] @@ -583,7 +583,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): From 8a8f38c0e82b9d7ac00b6d37dc6a3fb4a3606839 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Sun, 27 Mar 2022 00:11:51 +0800 Subject: [PATCH 13/60] first --- pandas/tests/arithmetic/test_interval.py | 24 ++- pandas/tests/arrays/interval/test_interval.py | 26 +-- pandas/tests/arrays/test_array.py | 2 +- pandas/tests/dtypes/test_common.py | 2 +- pandas/tests/dtypes/test_dtypes.py | 22 +-- pandas/tests/dtypes/test_inference.py | 20 +- .../tests/frame/methods/test_combine_first.py | 2 +- pandas/tests/frame/methods/test_sort_index.py | 2 +- pandas/tests/frame/test_constructors.py | 20 +- pandas/tests/groupby/test_grouping.py | 2 +- .../tests/indexes/categorical/test_astype.py | 4 +- .../tests/indexes/categorical/test_reindex.py | 10 +- pandas/tests/indexes/interval/test_astype.py | 28 ++- pandas/tests/indexes/interval/test_base.py | 10 +- .../indexes/interval/test_constructors.py | 65 +++---- pandas/tests/indexes/interval/test_equals.py | 14 +- pandas/tests/indexes/interval/test_formats.py | 6 +- .../tests/indexes/interval/test_indexing.py | 100 +++++----- .../tests/indexes/interval/test_interval.py | 172 +++++++++--------- .../indexes/interval/test_interval_range.py | 6 +- .../indexes/interval/test_interval_tree.py | 8 +- pandas/tests/indexes/interval/test_pickle.py | 6 +- pandas/tests/indexes/interval/test_setops.py | 12 +- pandas/tests/indexes/test_base.py | 8 +- .../indexing/interval/test_interval_new.py | 24 ++- pandas/tests/indexing/test_coercion.py | 4 +- pandas/tests/internals/test_internals.py | 2 +- pandas/tests/reshape/test_cut.py | 32 ++-- pandas/tests/reshape/test_qcut.py | 2 +- pandas/tests/scalar/interval/test_interval.py | 42 ++--- pandas/tests/series/indexing/test_setitem.py | 2 +- pandas/tests/series/test_constructors.py | 4 +- 32 files changed, 366 insertions(+), 317 deletions(-) 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 2b5712e76e8cc..60278b48a84fd 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) @@ -139,7 +139,7 @@ def test_setitem_mismatched_closed(self): 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): @@ -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,11 +386,11 @@ 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) diff --git a/pandas/tests/arrays/test_array.py b/pandas/tests/arrays/test_array.py index 3c8d12556bf1c..0d8e6576bd823 100644 --- a/pandas/tests/arrays/test_array.py +++ b/pandas/tests/arrays/test_array.py @@ -296,7 +296,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, inclusive="left"), pd.Interval(1, 2, inclusive="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/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..58e5f35ad5b24 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -568,7 +568,7 @@ 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) @@ -579,10 +579,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 +624,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 +729,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 +748,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" 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/frame/methods/test_combine_first.py b/pandas/tests/frame/methods/test_combine_first.py index daddca7891b93..0650b070d4325 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_sort_index.py b/pandas/tests/frame/methods/test_sort_index.py index bc5c68e4ec18d..9e2391ee69626 100644 --- a/pandas/tests/frame/methods/test_sort_index.py +++ b/pandas/tests/frame/methods/test_sort_index.py @@ -381,7 +381,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) diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 82c7117cc00c6..f23c86cf440c7 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -2410,16 +2410,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 efb0b82f58e97..3bf6b166acf65 100644 --- a/pandas/tests/groupby/test_grouping.py +++ b/pandas/tests/groupby/test_grouping.py @@ -792,7 +792,7 @@ def test_get_group_empty_bins(self, observed): 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)) 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 c253a745ef5a2..f52fe0354ee5c 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, closed="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..1bee8b4a579dc 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])) @@ -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) @@ -183,10 +183,10 @@ def test_generic_errors(self, constructor): # filler input data to be used when supplying invalid kwargs filler = self.get_kwargs_from_breaks(range(10)) - # invalid closed - msg = "closed must be one of 'right', 'left', 'both', 'neither'" + # invalid inclusive + 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="right"): """ 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="right"): """ 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="right"): """ 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="right"): """ 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) @@ -410,29 +410,32 @@ def test_constructor_errors(self, constructor): @pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning") @pytest.mark.parametrize( - "data, closed", + "data, inclusive", [ ([], "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): + def test_override_inferred_closed(self, constructor, data, inclusive): # GH 19370 if isinstance(data, IntervalIndex): 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=inclusive) + result = constructor(data, inclusive=inclusive) 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..a0465cdd9a977 100644 --- a/pandas/tests/indexes/interval/test_formats.py +++ b/pandas/tests/indexes/interval/test_formats.py @@ -65,7 +65,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 +97,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..372b6618e851a 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,7 +278,7 @@ 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"], ], @@ -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") @@ -439,7 +453,7 @@ def test_slice_locs_with_interval(self): 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)) @@ -448,7 +462,7 @@ def test_slice_locs_with_interval(self): 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)) @@ -459,7 +473,7 @@ def test_slice_locs_with_interval(self): 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)) @@ -468,7 +482,7 @@ def test_slice_locs_with_interval(self): 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)) @@ -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 37c13c37d070b..7ed66a4b8cf26 100644 --- a/pandas/tests/indexes/interval/test_interval.py +++ b/pandas/tests/indexes/interval/test_interval.py @@ -30,19 +30,19 @@ def name(request): class TestIntervalIndex: index = IntervalIndex.from_arrays([0, 1], [1, 2]) - 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) @@ -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) @@ -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) @@ -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, closed=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 diff --git a/pandas/tests/indexes/interval/test_interval_range.py b/pandas/tests/indexes/interval/test_interval_range.py index 2f28c33a3bbc6..cc13690d19afe 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( diff --git a/pandas/tests/indexes/interval/test_interval_tree.py b/pandas/tests/indexes/interval/test_interval_tree.py index f2d9ec3608271..56aa586501b17 100644 --- a/pandas/tests/indexes/interval/test_interval_tree.py +++ b/pandas/tests/indexes/interval/test_interval_tree.py @@ -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") 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 059b0b75f4190..56fceacb5b1dd 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 03dc2d5f1a617..3f2f9f16f8362 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -1432,10 +1432,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_new.py b/pandas/tests/indexing/interval/test_interval_new.py index aad6523357df6..ee00328dd43e5 100644 --- a/pandas/tests/indexing/interval/test_interval_new.py +++ b/pandas/tests/indexing/interval/test_interval_new.py @@ -33,18 +33,24 @@ def test_loc_with_interval(self, series_with_interval_index, indexer_sl): 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')")): + with pytest.raises( + KeyError, match=re.escape("Interval(3, 5, inclusive='right')") + ): indexer_sl(ser)[Interval(3, 5)] 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)] - with pytest.raises(KeyError, match=re.escape("Interval(5, 6, closed='right')")): + with pytest.raises( + KeyError, match=re.escape("Interval(5, 6, inclusive='right')") + ): indexer_sl(ser)[Interval(5, 6)] def test_loc_with_scalar(self, series_with_interval_index, indexer_sl): @@ -96,7 +102,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 @@ -147,10 +153,12 @@ def test_loc_with_overlap(self, indexer_sl): result = indexer_sl(ser)[[Interval(1, 5), Interval(3, 7)]] tm.assert_series_equal(expected, result) - with pytest.raises(KeyError, match=re.escape("Interval(3, 5, closed='right')")): + with pytest.raises( + KeyError, match=re.escape("Interval(3, 5, inclusive='right')") + ): indexer_sl(ser)[Interval(3, 5)] - 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)]] diff --git a/pandas/tests/indexing/test_coercion.py b/pandas/tests/indexing/test_coercion.py index 2d54a9ba370ca..21c36f2fc0d37 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 f4060c84f533a..ea8baf91c5f0f 100644 --- a/pandas/tests/internals/test_internals.py +++ b/pandas/tests/internals/test_internals.py @@ -1272,7 +1272,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/test_cut.py b/pandas/tests/reshape/test_cut.py index 1425686f027e4..4f2404dba8cdb 100644 --- a/pandas/tests/reshape/test_cut.py +++ b/pandas/tests/reshape/test_cut.py @@ -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) @@ -232,7 +232,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) @@ -355,7 +355,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 +368,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) @@ -685,8 +685,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 +706,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_qcut.py b/pandas/tests/reshape/test_qcut.py index f7c7204d02a49..2e6110000a123 100644 --- a/pandas/tests/reshape/test_qcut.py +++ b/pandas/tests/reshape/test_qcut.py @@ -198,7 +198,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) diff --git a/pandas/tests/scalar/interval/test_interval.py b/pandas/tests/scalar/interval/test_interval.py index 1f76a7df1e996..3d1718ed3077e 100644 --- a/pandas/tests/scalar/interval/test_interval.py +++ b/pandas/tests/scalar/interval/test_interval.py @@ -18,17 +18,17 @@ def interval(): 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, inclusive="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, inclusive="both") assert 0 in interval_both assert 1 in interval_both - interval_neither = Interval(0, 1, closed="neither") + interval_neither = Interval(0, 1, inclusive="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) == Interval(0, 1, inclusive="right") + assert Interval(0, 1) != Interval(0, 1, inclusive="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, inclusive=closed) + expected = Interval(1, 2, inclusive=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, inclusive=closed) + expected = Interval(-1, 0, inclusive=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, inclusive=closed) + expected = Interval(0, 2, inclusive=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, inclusive=closed) + expected = Interval(0, 0.5, inclusive=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, inclusive=closed) + expected = Interval(0, 1, inclusive=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, inclusive="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 992f67c2affc6..8188b2c35c0df 100644 --- a/pandas/tests/series/indexing/test_setitem.py +++ b/pandas/tests/series/indexing/test_setitem.py @@ -1348,7 +1348,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): diff --git a/pandas/tests/series/test_constructors.py b/pandas/tests/series/test_constructors.py index e416b1f625993..a9c97a830130f 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)) @@ -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 From 97f7b9ef326f0328fc80b5ad8b9c88aa7d436997 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Sun, 27 Mar 2022 13:43:19 +0800 Subject: [PATCH 14/60] add --- doc/redirects.csv | 4 ++-- doc/source/reference/arrays.rst | 4 ++-- doc/source/reference/indexing.rst | 2 +- doc/source/user_guide/advanced.rst | 2 +- doc/source/whatsnew/v0.25.0.rst | 14 +++++++------- pandas/core/arrays/interval.py | 3 ++- pandas/core/dtypes/dtypes.py | 5 ++++- 7 files changed, 19 insertions(+), 15 deletions(-) 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 b8df21ab5a5b4..ede49fc280403 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.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index 9cbfa49cc8c5c..9093c2ada7ff0 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -579,18 +579,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. @@ -614,7 +614,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. @@ -675,11 +675,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/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index ef5cb511baa93..1e6beedf6bad3 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -1491,7 +1491,8 @@ def __arrow_array__(self, type=None): raise TypeError( "Not supported to convert IntervalArray to type with " f"different 'subtype' ({self.dtype.subtype} vs {type.subtype}) " - f"and 'inclusive' ({self.inclusive} vs {type.inclusive}) attributes" + f"and 'inclusive' ({self.inclusive} vs {type.inclusive}) " + f"attributes" ) else: raise TypeError( diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 9312d4fbd1d87..4e328856a2b5e 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1052,7 +1052,10 @@ class IntervalDtype(PandasExtensionDtype): "inclusive", ) _match = re.compile( - r"(I|i)nterval\[(?P[^,]+)(, (?P(right|left|both|neither)))?\]" + r""" + (I|i)nterval\[(?P[^,]+) + (, (?P(right|left|both|neither)))?\] + """ ) _cache_dtypes: dict[str_type, PandasExtensionDtype] = {} From a8a3266841cf869f9c773f81afe61d313339d180 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Sun, 27 Mar 2022 14:24:26 +0800 Subject: [PATCH 15/60] pre commit --- pandas/core/arrays/interval.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 1e6beedf6bad3..e02efc9185121 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -1491,7 +1491,7 @@ def __arrow_array__(self, type=None): raise TypeError( "Not supported to convert IntervalArray to type with " f"different 'subtype' ({self.dtype.subtype} vs {type.subtype}) " - f"and 'inclusive' ({self.inclusive} vs {type.inclusive}) " + f"and 'inclusive' ({self.inclusive} vs {type.inclusive}) " f"attributes" ) else: From a4d58dc8f5904a5e3e624dfec9d042d6e6ec12cc Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Sun, 27 Mar 2022 15:03:16 +0800 Subject: [PATCH 16/60] pre commit --- pandas/core/dtypes/dtypes.py | 11 +++++------ pandas/core/indexes/interval.py | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 4e328856a2b5e..1a2218db75281 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1051,12 +1051,11 @@ class IntervalDtype(PandasExtensionDtype): "subtype", "inclusive", ) - _match = re.compile( - r""" - (I|i)nterval\[(?P[^,]+) - (, (?P(right|left|both|neither)))?\] - """ - ) + MATCH_PATTERN = r""" + (I|i)nterval\[(?P[^,]+)(, + (?P(right|left|both|neither)))?\] + """ + _match = re.compile(r"^\s*" + MATCH_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) _cache_dtypes: dict[str_type, PandasExtensionDtype] = {} def __new__(cls, subtype=None, inclusive: str_type | None = None): diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index a77a47e52a3d7..79dc1f9e97061 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -443,9 +443,9 @@ def is_overlapping(self) -> bool: >>> index.is_overlapping True - Intervals that share inclusive endpoints overlap: + Intervals that share closed endpoints overlap: - >>> index = pd.interval_range(0, 3, inclusive='both') + >>> index = pd.interval_range(0, 3, closed='both') >>> index IntervalIndex([[0, 1], [1, 2], [2, 3]], dtype='interval[int64, both]') @@ -454,7 +454,7 @@ def is_overlapping(self) -> bool: Intervals that only have an open endpoint in common do not overlap: - >>> index = pd.interval_range(0, 3, inclusive='left') + >>> index = pd.interval_range(0, 3, closed='left') >>> index IntervalIndex([[0, 1), [1, 2), [2, 3)], dtype='interval[int64, left]') From edf53b7c65b31597356a1fd72823c2d22850f97d Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Sun, 27 Mar 2022 15:06:25 +0800 Subject: [PATCH 17/60] pre commit --- pandas/core/dtypes/dtypes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 1a2218db75281..9da63af8ffb81 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1051,8 +1051,9 @@ class IntervalDtype(PandasExtensionDtype): "subtype", "inclusive", ) + MATCH_PATTERN = r""" - (I|i)nterval\[(?P[^,]+)(, + (I|i)nterval\[(?P[^,]+)(, (?P(right|left|both|neither)))?\] """ _match = re.compile(r"^\s*" + MATCH_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) From d0f17c052491ea5d845e47830d2a0cc3b4673809 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Sun, 27 Mar 2022 15:06:37 +0800 Subject: [PATCH 18/60] pre commit --- pandas/core/dtypes/dtypes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 9da63af8ffb81..2162ba1107062 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1051,7 +1051,6 @@ class IntervalDtype(PandasExtensionDtype): "subtype", "inclusive", ) - MATCH_PATTERN = r""" (I|i)nterval\[(?P[^,]+)(, (?P(right|left|both|neither)))?\] From b00a94717e7885a7cfd2d59a2c9fae8a31b5b2c5 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Sun, 27 Mar 2022 16:09:49 +0800 Subject: [PATCH 19/60] pre commit --- pandas/core/dtypes/dtypes.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 2162ba1107062..34fe1b05ba922 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1051,11 +1051,10 @@ class IntervalDtype(PandasExtensionDtype): "subtype", "inclusive", ) - MATCH_PATTERN = r""" - (I|i)nterval\[(?P[^,]+)(, - (?P(right|left|both|neither)))?\] - """ - _match = re.compile(r"^\s*" + MATCH_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) + _match = re.compile( + r"(I|i)nterval\[(?P[^,]+)(, (" + r"?P(right|left|both|neither)))?\]" + ) _cache_dtypes: dict[str_type, PandasExtensionDtype] = {} def __new__(cls, subtype=None, inclusive: str_type | None = None): From 8f3991d22893d85e5501a3d4083a905d25e604a9 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Sun, 27 Mar 2022 23:42:27 +0800 Subject: [PATCH 20/60] interval --- doc/source/whatsnew/v1.5.0.rst | 7 +- pandas/_libs/interval.pyx | 41 +++++++++-- pandas/_libs/intervaltree.pxi.in | 27 +++++++- pandas/core/arrays/_arrow_utils.py | 32 ++++++++- pandas/core/arrays/interval.py | 52 +++++++++++++- pandas/core/dtypes/dtypes.py | 36 +++++++++- pandas/core/indexes/interval.py | 106 ++++++++++++++++++++++++++++- 7 files changed, 287 insertions(+), 14 deletions(-) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index dbf9547f561d2..f248ca884bc2c 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -364,7 +364,12 @@ Other Deprecations - Deprecated behavior of method :meth:`DataFrame.quantile`, attribute ``numeric_only`` will default False. Including datetime/timedelta columns in the result (:issue:`7308`). - Deprecated :attr:`Timedelta.freq` and :attr:`Timedelta.is_populated` (:issue:`46430`) - Deprecated :attr:`Timedelta.delta` (:issue:`46476`) -- +- Deprecated the ``closed`` argument in :meth:`Interval.__init__` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) +- Deprecated the ``closed`` argument in :meth:`IntervalIndex.__new__`, :meth:`IntervalIndex.from_breaks`, :meth:`IntervalIndex.from_arrays` and :meth:`IntervalIndex.from_tuples` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) +- Deprecated the ``closed`` argument in :meth:`IntervalDtype.__new__` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) +- Deprecated the ``closed`` argument in :meth:`IntervalArray.__new__` and :meth:`IntervalArray._simple_new`, in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) +- Deprecated the ``closed`` argument in :meth:`intervaltree.__init__` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) +- Deprecated the ``closed`` argument in :meth:`ArrowInterval.__init__` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) .. --------------------------------------------------------------------------- .. _whatsnew_150.performance: diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 2d5d93de53934..2146c67eab297 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: @@ -222,10 +224,18 @@ cdef class Interval(IntervalMixin): Left bound for the interval. right : orderable scalar Right bound for the interval. - inclusive : {'right', 'left', 'both', 'neither'}, default 'right' + closed : {'right', 'left', 'both', 'neither'}, default 'right' 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 @@ -255,7 +265,7 @@ cdef class Interval(IntervalMixin): -------- 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, inclusive='right') @@ -318,13 +328,36 @@ cdef class Interval(IntervalMixin): neither. """ - def __init__(self, left, right, str inclusive='right'): + def __init__(self, left, right, closed : lib.NoDefault = lib.no_default, inclusive: str | None = None): # 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 inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + 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', 'right'," + "or 'both'" + ) + elif inclusive is None: + inclusive = "both" + if inclusive not in VALID_CLOSED: raise ValueError(f"invalid option for 'inclusive': {inclusive}") if not left <= right: diff --git a/pandas/_libs/intervaltree.pxi.in b/pandas/_libs/intervaltree.pxi.in index dcbef3cc85cb2..31f0600b42f1b 100644 --- a/pandas/_libs/intervaltree.pxi.in +++ b/pandas/_libs/intervaltree.pxi.in @@ -3,7 +3,9 @@ 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 ctypedef fused int_scalar_t: @@ -38,7 +40,7 @@ cdef class IntervalTree(IntervalMixin): object _is_overlapping, _left_sorter, _right_sorter Py_ssize_t _na_count - def __init__(self, left, right, inclusive='right', leaf_size=100): + def __init__(self, left, right, closed : lib.NoDefault = lib.no_default, inclusive: str | None = None, leaf_size=100): """ Parameters ---------- @@ -53,6 +55,29 @@ cdef class IntervalTree(IntervalMixin): to brute-force search. Tune this parameter to optimize query performance. """ + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + 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', 'right'," + "or 'both'" + ) + elif inclusive is None: + inclusive = "both" + if inclusive not in ['left', 'right', 'both', 'neither']: raise ValueError("invalid option for 'inclusive': %s" % inclusive) diff --git a/pandas/core/arrays/_arrow_utils.py b/pandas/core/arrays/_arrow_utils.py index d27dee731221f..8d8470489a97c 100644 --- a/pandas/core/arrays/_arrow_utils.py +++ b/pandas/core/arrays/_arrow_utils.py @@ -1,10 +1,13 @@ from __future__ import annotations import json +import warnings import numpy as np import pyarrow +from pandas._libs import lib + from pandas.core.arrays.interval import VALID_CLOSED @@ -88,9 +91,36 @@ def to_pandas_dtype(self): class ArrowIntervalType(pyarrow.ExtensionType): - def __init__(self, subtype, inclusive) -> None: + def __init__( + self, + subtype, + closed: lib.NoDefault = lib.no_default, + inclusive: str | None = None, + ) -> None: # attributes need to be set first before calling # super init (as that calls serialize) + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + 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', 'right'," + "or 'both'" + ) + elif inclusive is None: + inclusive = "both" assert inclusive in VALID_CLOSED self._closed = inclusive if not isinstance(subtype, pyarrow.DataType): diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index e02efc9185121..ac921a52db2e5 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -14,6 +14,7 @@ cast, overload, ) +import warnings import numpy as np @@ -217,11 +218,34 @@ class IntervalArray(IntervalMixin, ExtensionArray): def __new__( cls: type[IntervalArrayT], data, - inclusive=None, + closed: lib.NoDefault = lib.no_default, + inclusive: str | None = None, dtype: Dtype | None = None, copy: bool = False, verify_integrity: bool = True, ): + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + 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', 'right'," + "or 'both'" + ) + elif inclusive is None: + inclusive = "both" data = extract_array(data, extract_numpy=True) @@ -263,13 +287,37 @@ def _simple_new( cls: type[IntervalArrayT], left, right, - inclusive=None, + closed: lib.NoDefault = lib.no_default, + inclusive: str | None = None, copy: bool = False, dtype: Dtype | None = None, verify_integrity: bool = True, ) -> IntervalArrayT: result = IntervalMixin.__new__(cls) + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + 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', 'right'," + "or 'both'" + ) + elif inclusive is None: + inclusive = "both" + if inclusive is None and isinstance(dtype, IntervalDtype): inclusive = dtype.inclusive diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 34fe1b05ba922..6b31a34bbfc89 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -10,11 +10,15 @@ MutableMapping, cast, ) +import warnings import numpy as np import pytz -from pandas._libs import missing as libmissing +from pandas._libs import ( + lib, + missing as libmissing, +) from pandas._libs.interval import Interval from pandas._libs.properties import cache_readonly from pandas._libs.tslibs import ( @@ -1057,12 +1061,40 @@ class IntervalDtype(PandasExtensionDtype): ) _cache_dtypes: dict[str_type, PandasExtensionDtype] = {} - def __new__(cls, subtype=None, inclusive: str_type | None = None): + def __new__( + cls, + subtype=None, + closed: lib.NoDefault = lib.no_default, + inclusive: str | None = None, + ): from pandas.core.dtypes.common import ( is_string_dtype, pandas_dtype, ) + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + 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', 'right'," + "or 'both'" + ) + elif inclusive is None: + inclusive = "both" + if inclusive is not None and inclusive not in { "right", "left", diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 79dc1f9e97061..f934d710c7522 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -11,6 +11,7 @@ Hashable, Literal, ) +import warnings import numpy as np @@ -211,6 +212,7 @@ class IntervalIndex(ExtensionIndex): def __new__( cls, data, + closed: lib.NoDefault = lib.no_default, inclusive=None, dtype: Dtype | None = None, copy: bool = False, @@ -218,6 +220,29 @@ def __new__( verify_integrity: bool = True, ) -> IntervalIndex: + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + 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', 'right'," + "or 'both'" + ) + elif inclusive is None: + inclusive = "both" + name = maybe_extract_name(name, data, cls) with rewrite_exception("IntervalArray", cls.__name__): @@ -250,11 +275,36 @@ def __new__( def from_breaks( cls, breaks, - inclusive: str = "right", + closed: lib.NoDefault = lib.no_default, + inclusive: str = None, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, ) -> IntervalIndex: + + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + 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', 'right'," + "or 'both'" + ) + elif inclusive is None: + inclusive = "both" + with rewrite_exception("IntervalArray", cls.__name__): array = IntervalArray.from_breaks( breaks, inclusive=inclusive, copy=copy, dtype=dtype @@ -281,11 +331,36 @@ def from_arrays( cls, left, right, - inclusive: str = "right", + closed: lib.NoDefault = lib.no_default, + inclusive: str = None, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, ) -> IntervalIndex: + + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + 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', 'right'," + "or 'both'" + ) + elif inclusive is None: + inclusive = "both" + with rewrite_exception("IntervalArray", cls.__name__): array = IntervalArray.from_arrays( left, right, inclusive, copy=copy, dtype=dtype @@ -311,11 +386,36 @@ def from_arrays( def from_tuples( cls, data, - inclusive: str = "right", + closed: lib.NoDefault = lib.no_default, + inclusive: str = None, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, ) -> IntervalIndex: + + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + 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', 'right'," + "or 'both'" + ) + elif inclusive is None: + inclusive = "both" + with rewrite_exception("IntervalArray", cls.__name__): arr = IntervalArray.from_tuples( data, inclusive=inclusive, copy=copy, dtype=dtype From 5bc735b0dda722c77270d90d1e9ca5ee2d4094d5 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Sun, 27 Mar 2022 23:52:04 +0800 Subject: [PATCH 21/60] doc --- pandas/_libs/interval.pyx | 2 +- pandas/_libs/intervaltree.pxi.in | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 2146c67eab297..40af58830023f 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -188,7 +188,7 @@ cdef class IntervalMixin: 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'. diff --git a/pandas/_libs/intervaltree.pxi.in b/pandas/_libs/intervaltree.pxi.in index 31f0600b42f1b..83f70d9594c6a 100644 --- a/pandas/_libs/intervaltree.pxi.in +++ b/pandas/_libs/intervaltree.pxi.in @@ -47,9 +47,18 @@ cdef class IntervalTree(IntervalMixin): left, right : np.ndarray[ndim=1] Left and right bounds for each interval. Assumed to contain no NaNs. - inclusive : {'left', 'right', 'both', 'neither'}, optional + 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 From eb3d9eceb61b12f431f586b8d4d4bf7d875e699b Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Sun, 27 Mar 2022 23:58:32 +0800 Subject: [PATCH 22/60] doc --- pandas/core/dtypes/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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])) From 0a41668f77a9fa01163735ff849e7004e1f0dd36 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Mon, 28 Mar 2022 00:04:53 +0800 Subject: [PATCH 23/60] doc --- pandas/_libs/interval.pyx | 4 ++-- pandas/_libs/intervaltree.pxi.in | 4 ++-- pandas/core/arrays/_arrow_utils.py | 4 ++-- pandas/core/arrays/interval.py | 8 ++++---- pandas/core/dtypes/dtypes.py | 4 ++-- pandas/core/indexes/interval.py | 16 ++++++++-------- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 40af58830023f..c0a1ae8cfee5c 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -352,8 +352,8 @@ cdef class Interval(IntervalMixin): inclusive = closed else: raise ValueError( - "Argument `closed` has to be either 'both', 'neither', 'left', 'right'," - "or 'both'" + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: inclusive = "both" diff --git a/pandas/_libs/intervaltree.pxi.in b/pandas/_libs/intervaltree.pxi.in index 83f70d9594c6a..6122571872773 100644 --- a/pandas/_libs/intervaltree.pxi.in +++ b/pandas/_libs/intervaltree.pxi.in @@ -81,8 +81,8 @@ cdef class IntervalTree(IntervalMixin): inclusive = closed else: raise ValueError( - "Argument `closed` has to be either 'both', 'neither', 'left', 'right'," - "or 'both'" + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: inclusive = "both" diff --git a/pandas/core/arrays/_arrow_utils.py b/pandas/core/arrays/_arrow_utils.py index 8d8470489a97c..084fcf10a7fca 100644 --- a/pandas/core/arrays/_arrow_utils.py +++ b/pandas/core/arrays/_arrow_utils.py @@ -116,8 +116,8 @@ def __init__( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either 'both', 'neither', 'left', 'right'," - "or 'both'" + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: inclusive = "both" diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index ac921a52db2e5..93fff67e94704 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -241,8 +241,8 @@ def __new__( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either 'both', 'neither', 'left', 'right'," - "or 'both'" + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: inclusive = "both" @@ -312,8 +312,8 @@ def _simple_new( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either 'both', 'neither', 'left', 'right'," - "or 'both'" + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: inclusive = "both" diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 6b31a34bbfc89..f8189a95be907 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1089,8 +1089,8 @@ def __new__( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either 'both', 'neither', 'left', 'right'," - "or 'both'" + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: inclusive = "both" diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index f934d710c7522..be6dfc9b33246 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -237,8 +237,8 @@ def __new__( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either 'both', 'neither', 'left', 'right'," - "or 'both'" + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: inclusive = "both" @@ -299,8 +299,8 @@ def from_breaks( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either 'both', 'neither', 'left', 'right'," - "or 'both'" + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: inclusive = "both" @@ -355,8 +355,8 @@ def from_arrays( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either 'both', 'neither', 'left', 'right'," - "or 'both'" + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: inclusive = "both" @@ -410,8 +410,8 @@ def from_tuples( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either 'both', 'neither', 'left', 'right'," - "or 'both'" + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: inclusive = "both" From c1f9de4646aa4e0a3db7930f758af69bfb1a8fe3 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Mon, 28 Mar 2022 00:25:19 +0800 Subject: [PATCH 24/60] pre commit --- pandas/core/arrays/_arrow_utils.py | 1 + pandas/core/arrays/interval.py | 1 + pandas/core/dtypes/dtypes.py | 1 + pandas/core/indexes/interval.py | 1 + 4 files changed, 4 insertions(+) diff --git a/pandas/core/arrays/_arrow_utils.py b/pandas/core/arrays/_arrow_utils.py index 084fcf10a7fca..c862d690df49e 100644 --- a/pandas/core/arrays/_arrow_utils.py +++ b/pandas/core/arrays/_arrow_utils.py @@ -1,3 +1,4 @@ + from __future__ import annotations import json diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 93fff67e94704..69e5c1dbfd37c 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -1,3 +1,4 @@ + from __future__ import annotations import operator diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index f8189a95be907..f332746763a50 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1,3 +1,4 @@ + """ Define extension dtypes. """ diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index be6dfc9b33246..e04691facbe36 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -1,3 +1,4 @@ + """ define the IntervalIndex """ from __future__ import annotations From 7d9db8b055fe83217032825066c51c6e06a52151 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Mon, 28 Mar 2022 00:28:56 +0800 Subject: [PATCH 25/60] pre commit --- pandas/core/arrays/_arrow_utils.py | 3 +-- pandas/core/arrays/interval.py | 5 ++--- pandas/core/dtypes/dtypes.py | 3 +-- pandas/core/indexes/interval.py | 9 ++++----- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pandas/core/arrays/_arrow_utils.py b/pandas/core/arrays/_arrow_utils.py index c862d690df49e..45f272afbe39d 100644 --- a/pandas/core/arrays/_arrow_utils.py +++ b/pandas/core/arrays/_arrow_utils.py @@ -1,4 +1,3 @@ - from __future__ import annotations import json @@ -117,7 +116,7 @@ def __init__( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either" + "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 69e5c1dbfd37c..5df763dce0987 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -1,4 +1,3 @@ - from __future__ import annotations import operator @@ -242,7 +241,7 @@ def __new__( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either" + "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: @@ -313,7 +312,7 @@ def _simple_new( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either" + "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index f332746763a50..b3d11560f7804 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1,4 +1,3 @@ - """ Define extension dtypes. """ @@ -1090,7 +1089,7 @@ def __new__( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either" + "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index e04691facbe36..4a635b3e566a7 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -1,4 +1,3 @@ - """ define the IntervalIndex """ from __future__ import annotations @@ -238,7 +237,7 @@ def __new__( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either" + "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: @@ -300,7 +299,7 @@ def from_breaks( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either" + "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: @@ -356,7 +355,7 @@ def from_arrays( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either" + "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: @@ -411,7 +410,7 @@ def from_tuples( inclusive = closed else: raise ValueError( - "Argument `closed` has to be either" + "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: From 49b35cf3360dac5e000693165cbdc20b70ea84d9 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Mon, 28 Mar 2022 00:40:44 +0800 Subject: [PATCH 26/60] pre commit --- pandas/_libs/interval.pyx | 2 +- pandas/_libs/intervaltree.pxi.in | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index c0a1ae8cfee5c..5c266a3768351 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -352,7 +352,7 @@ cdef class Interval(IntervalMixin): inclusive = closed else: raise ValueError( - "Argument `closed` has to be either" + "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: diff --git a/pandas/_libs/intervaltree.pxi.in b/pandas/_libs/intervaltree.pxi.in index 6122571872773..b84087a2a01bf 100644 --- a/pandas/_libs/intervaltree.pxi.in +++ b/pandas/_libs/intervaltree.pxi.in @@ -81,7 +81,7 @@ cdef class IntervalTree(IntervalMixin): inclusive = closed else: raise ValueError( - "Argument `closed` has to be either" + "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) elif inclusive is None: From 5703c010ccd5af589a679b2ed2387ced78573407 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Mon, 28 Mar 2022 23:23:59 +0800 Subject: [PATCH 27/60] test --- pandas/core/arrays/interval.py | 23 ++++++++++++----------- pandas/core/indexes/interval.py | 2 -- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 5df763dce0987..c493fb4f1d9dc 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -244,8 +244,6 @@ def __new__( "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) - elif inclusive is None: - inclusive = "both" data = extract_array(data, extract_numpy=True) @@ -276,7 +274,7 @@ def __new__( return cls._simple_new( left, right, - inclusive, + inclusive=inclusive, copy=copy, dtype=dtype, verify_integrity=verify_integrity, @@ -315,13 +313,12 @@ def _simple_new( "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) - elif inclusive is None: - inclusive = "both" if inclusive is None and isinstance(dtype, IntervalDtype): inclusive = dtype.inclusive - inclusive = inclusive or "right" + inclusive = inclusive or "both" + left = ensure_index(left, copy=copy) right = ensure_index(right, copy=copy) @@ -336,7 +333,6 @@ def _simple_new( else: msg = f"dtype must be an IntervalDtype, got {dtype}" raise TypeError(msg) - if dtype.inclusive is None: # possibly loading an old pickle dtype = IntervalDtype(dtype.subtype, inclusive) @@ -542,7 +538,7 @@ def from_arrays( cls: type[IntervalArrayT], left, right, - inclusive="right", + inclusive="both", copy: bool = False, dtype: Dtype | None = None, ) -> IntervalArrayT: @@ -550,7 +546,12 @@ def from_arrays( right = _maybe_convert_platform_interval(right) return cls._simple_new( - left, right, inclusive, 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( @@ -722,7 +723,7 @@ def __getitem__( # scalar if is_scalar(left) and isna(left): return self._fill_value - return Interval(left, right, self.inclusive) + 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") @@ -1493,7 +1494,7 @@ def __array__(self, dtype: NpDtype | None = None) -> np.ndarray: if mask[i]: result[i] = np.nan else: - result[i] = Interval(left[i], right[i], inclusive) + result[i] = Interval(left[i], right[i], inclusive=inclusive) return result def __arrow_array__(self, type=None): diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 4a635b3e566a7..7e2642a450fcd 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -240,8 +240,6 @@ def __new__( "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) - elif inclusive is None: - inclusive = "both" name = maybe_extract_name(name, data, cls) From 816da28fda26c02c06b96b31266095f9f354dbab Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Tue, 29 Mar 2022 22:50:03 +0800 Subject: [PATCH 28/60] test --- pandas/_libs/interval.pyx | 2 +- pandas/_libs/intervaltree.pxi.in | 2 +- pandas/conftest.py | 10 ++++- pandas/core/arrays/_arrow_utils.py | 4 +- pandas/core/arrays/interval.py | 4 +- pandas/core/dtypes/dtypes.py | 4 +- pandas/core/indexes/interval.py | 8 ++-- .../indexes/interval/test_constructors.py | 42 +++++++++---------- pandas/tests/test_algos.py | 13 ++++-- 9 files changed, 48 insertions(+), 41 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 5c266a3768351..a9d3ff5c5f000 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -328,7 +328,7 @@ cdef class Interval(IntervalMixin): neither. """ - def __init__(self, left, right, closed : lib.NoDefault = lib.no_default, inclusive: str | None = None): + def __init__(self, left, right, inclusive: str | None = None, closed: 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 diff --git a/pandas/_libs/intervaltree.pxi.in b/pandas/_libs/intervaltree.pxi.in index b84087a2a01bf..2a4bd9a3c9a2c 100644 --- a/pandas/_libs/intervaltree.pxi.in +++ b/pandas/_libs/intervaltree.pxi.in @@ -40,7 +40,7 @@ cdef class IntervalTree(IntervalMixin): object _is_overlapping, _left_sorter, _right_sorter Py_ssize_t _na_count - def __init__(self, left, right, closed : lib.NoDefault = lib.no_default, inclusive: str | None = None, leaf_size=100): + def __init__(self, left, right, inclusive: str | None = None, closed: lib.NoDefault = lib.no_default, leaf_size=100): """ Parameters ---------- diff --git a/pandas/conftest.py b/pandas/conftest.py index 8c10a0375d4da..d362543b76d8f 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -890,8 +890,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_utils.py b/pandas/core/arrays/_arrow_utils.py index 45f272afbe39d..b3b1b6b07af42 100644 --- a/pandas/core/arrays/_arrow_utils.py +++ b/pandas/core/arrays/_arrow_utils.py @@ -94,8 +94,8 @@ class ArrowIntervalType(pyarrow.ExtensionType): def __init__( self, subtype, - closed: lib.NoDefault = lib.no_default, inclusive: str | None = None, + closed: lib.NoDefault = lib.no_default, ) -> None: # attributes need to be set first before calling # super init (as that calls serialize) @@ -119,8 +119,6 @@ def __init__( "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) - elif inclusive is None: - inclusive = "both" assert inclusive in VALID_CLOSED self._closed = inclusive if not isinstance(subtype, pyarrow.DataType): diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index c493fb4f1d9dc..cf2a116bf7afd 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -218,8 +218,8 @@ class IntervalArray(IntervalMixin, ExtensionArray): def __new__( cls: type[IntervalArrayT], data, - closed: lib.NoDefault = lib.no_default, inclusive: str | None = None, + closed: lib.NoDefault = lib.no_default, dtype: Dtype | None = None, copy: bool = False, verify_integrity: bool = True, @@ -285,8 +285,8 @@ def _simple_new( cls: type[IntervalArrayT], left, right, - closed: lib.NoDefault = lib.no_default, inclusive: str | None = None, + closed: lib.NoDefault = lib.no_default, copy: bool = False, dtype: Dtype | None = None, verify_integrity: bool = True, diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index b3d11560f7804..2cf0a8e903c54 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1064,8 +1064,8 @@ class IntervalDtype(PandasExtensionDtype): def __new__( cls, subtype=None, - closed: lib.NoDefault = lib.no_default, inclusive: str | None = None, + closed: lib.NoDefault = lib.no_default, ): from pandas.core.dtypes.common import ( is_string_dtype, @@ -1092,8 +1092,6 @@ def __new__( "Argument `closed` has to be either" "'both', 'neither', 'left' or 'right'" ) - elif inclusive is None: - inclusive = "both" if inclusive is not None and inclusive not in { "right", diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 7e2642a450fcd..9d9e9e621479f 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -212,8 +212,8 @@ class IntervalIndex(ExtensionIndex): def __new__( cls, data, + inclusive: str = None, closed: lib.NoDefault = lib.no_default, - inclusive=None, dtype: Dtype | None = None, copy: bool = False, name: Hashable = None, @@ -273,8 +273,8 @@ def __new__( def from_breaks( cls, breaks, - closed: lib.NoDefault = lib.no_default, inclusive: str = None, + closed: lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, @@ -329,8 +329,8 @@ def from_arrays( cls, left, right, - closed: lib.NoDefault = lib.no_default, inclusive: str = None, + closed: lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, @@ -384,8 +384,8 @@ def from_arrays( def from_tuples( cls, data, - closed: lib.NoDefault = lib.no_default, inclusive: str = None, + closed: lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, diff --git a/pandas/tests/indexes/interval/test_constructors.py b/pandas/tests/indexes/interval/test_constructors.py index 1bee8b4a579dc..63fcda8f0f228 100644 --- a/pandas/tests/indexes/interval/test_constructors.py +++ b/pandas/tests/indexes/interval/test_constructors.py @@ -53,7 +53,7 @@ class ConstructorTests: ) def test_constructor(self, constructor, breaks, closed, name): result_kwargs = self.get_kwargs_from_breaks(breaks, closed) - result = constructor(inclusive=closed, name=name, **result_kwargs) + result = constructor(name=name, **result_kwargs) assert result.inclusive == closed assert result.name == name @@ -103,20 +103,19 @@ def test_constructor_pass_closed(self, constructor, breaks): iv_dtype = IntervalDtype(breaks.dtype) - result_kwargs = self.get_kwargs_from_breaks(breaks) + result_kwargs = self.get_kwargs_from_breaks(breaks, inclusive="left") for dtype in (iv_dtype, str(iv_dtype)): with tm.assert_produces_warning(warn): - - result = constructor(dtype=dtype, inclusive="left", **result_kwargs) + result = constructor(dtype=dtype, **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(inclusive=closed, **result_kwargs) + result_kwargs = self.get_kwargs_from_breaks(breaks, inclusive=closed) + result = constructor(**result_kwargs) expected_subtype = np.float64 expected_values = np.array(breaks[:-1], dtype=object) @@ -138,8 +137,8 @@ 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(inclusive=closed, **result_kwargs) + result_kwargs = self.get_kwargs_from_breaks(breaks, inclusive=closed) + result = constructor(**result_kwargs) expected_values = np.array([], dtype=object) expected_subtype = getattr(breaks, "dtype", np.int64) @@ -172,7 +171,7 @@ def test_constructor_categorical_valid(self, constructor, cat_constructor): # GH 21243/21253 breaks = np.arange(10, dtype="int64") - expected = IntervalIndex.from_breaks(breaks) + expected = IntervalIndex.from_breaks(breaks, inclusive="right") cat_breaks = cat_constructor(breaks) result_kwargs = self.get_kwargs_from_breaks(cat_breaks) @@ -181,12 +180,12 @@ def test_constructor_categorical_valid(self, constructor, cat_constructor): def test_generic_errors(self, constructor): # filler input data to be used when supplying invalid kwargs - filler = self.get_kwargs_from_breaks(range(10)) + filler = self.get_kwargs_from_breaks(range(10), inclusive="invalid") # invalid inclusive msg = "inclusive must be one of 'right', 'left', 'both', 'neither'" with pytest.raises(ValueError, match=msg): - constructor(inclusive="invalid", **filler) + constructor(**filler) # unsupported dtype msg = "dtype must be an IntervalDtype, got int64" @@ -224,7 +223,7 @@ def get_kwargs_from_breaks(self, breaks, inclusive="right"): converts intervals in breaks format to a dictionary of kwargs to specific to the format expected by IntervalIndex.from_arrays """ - return {"left": breaks[:-1], "right": breaks[1:]} + return {"left": breaks[:-1], "right": breaks[1:], "inclusive": inclusive} def test_constructor_errors(self): # GH 19016: categorical data @@ -273,7 +272,7 @@ def get_kwargs_from_breaks(self, breaks, inclusive="right"): converts intervals in breaks format to a dictionary of kwargs to specific to the format expected by IntervalIndex.from_breaks """ - return {"breaks": breaks} + return {"breaks": breaks, "inclusive": inclusive} def test_constructor_errors(self): # GH 19016: categorical data @@ -312,14 +311,14 @@ def get_kwargs_from_breaks(self, breaks, inclusive="right"): specific to the format expected by IntervalIndex.from_tuples """ if len(breaks) == 0: - return {"data": breaks} + return {"data": breaks, "inclusive": inclusive} tuples = list(zip(breaks[:-1], breaks[1:])) if isinstance(breaks, (list, tuple)): - return {"data": tuples} + return {"data": tuples, "inclusive": inclusive} elif is_categorical_dtype(breaks): - return {"data": breaks._constructor(tuples)} - return {"data": com.asarray_tuplesafe(tuples)} + return {"data": breaks._constructor(tuples), "inclusive": inclusive} + return {"data": com.asarray_tuplesafe(tuples), "inclusive": inclusive} def test_constructor_errors(self): # non-tuple @@ -362,18 +361,17 @@ def get_kwargs_from_breaks(self, breaks, inclusive="right"): specific to the format expected by the IntervalIndex/Index constructors """ if len(breaks) == 0: - return {"data": breaks} - + return {"data": breaks, "inclusive": inclusive} ivs = [ Interval(left, right, inclusive) if notna(left) else left for left, right in zip(breaks[:-1], breaks[1:]) ] if isinstance(breaks, list): - return {"data": ivs} + return {"data": ivs, "inclusive": inclusive} elif is_categorical_dtype(breaks): - return {"data": breaks._constructor(ivs)} - return {"data": np.array(ivs, dtype=object)} + return {"data": breaks._constructor(ivs), "inclusive": inclusive} + return {"data": np.array(ivs, dtype=object), "inclusive": inclusive} def test_generic_errors(self, constructor): """ diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index 7c16d13e59d3a..921907c8e5668 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) From 3efc78fe75013ed335761dc71738b48d74930753 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Wed, 30 Mar 2022 10:06:49 +0800 Subject: [PATCH 29/60] test --- pandas/tests/arrays/test_array.py | 7 +++-- .../tests/indexes/interval/test_indexing.py | 22 +++++++------- .../indexing/interval/test_interval_new.py | 30 ++++++++++--------- pandas/tests/reshape/concat/test_append.py | 2 +- pandas/tests/series/indexing/test_setitem.py | 13 +++++--- 5 files changed, 42 insertions(+), 32 deletions(-) diff --git a/pandas/tests/arrays/test_array.py b/pandas/tests/arrays/test_array.py index 0d8e6576bd823..f33eabf5454a2 100644 --- a/pandas/tests/arrays/test_array.py +++ b/pandas/tests/arrays/test_array.py @@ -133,7 +133,7 @@ ), # 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)]), ), @@ -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]), + ), # datetime ( [pd.Timestamp("2000"), pd.Timestamp("2001")], diff --git a/pandas/tests/indexes/interval/test_indexing.py b/pandas/tests/indexes/interval/test_indexing.py index 372b6618e851a..4cf754a7e52e0 100644 --- a/pandas/tests/indexes/interval/test_indexing.py +++ b/pandas/tests/indexes/interval/test_indexing.py @@ -285,7 +285,7 @@ def test_get_indexer_length_one_interval(self, size, closed): ) 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) @@ -294,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) @@ -447,7 +447,7 @@ 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, @@ -456,7 +456,7 @@ def test_slice_locs_with_interval(self): "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, @@ -465,9 +465,9 @@ def test_slice_locs_with_interval(self): "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, @@ -476,7 +476,7 @@ def test_slice_locs_with_interval(self): "Interval(0, 2, inclusive='right')\"" ), ): - index.slice_locs(end=Interval(0, 2)) + index.slice_locs(end=Interval(0, 2, "right")) with pytest.raises( KeyError, @@ -485,7 +485,7 @@ def test_slice_locs_with_interval(self): "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)]) @@ -499,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) @@ -509,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) @@ -530,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=( diff --git a/pandas/tests/indexing/interval/test_interval_new.py b/pandas/tests/indexing/interval/test_interval_new.py index ee00328dd43e5..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,11 +27,11 @@ 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 @@ -41,17 +43,17 @@ def test_loc_with_interval(self, series_with_interval_index, indexer_sl): with pytest.raises( KeyError, match=re.escape("Interval(3, 5, inclusive='right')") ): - indexer_sl(ser)[Interval(3, 5)] + indexer_sl(ser)[Interval(3, 5, "right")] with pytest.raises( 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, inclusive='right')") ): - indexer_sl(ser)[Interval(5, 6)] + indexer_sl(ser)[Interval(5, 6, "right")] def test_loc_with_scalar(self, series_with_interval_index, indexer_sl): @@ -90,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" @@ -133,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 @@ -146,25 +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, inclusive='right')") ): - indexer_sl(ser)[Interval(3, 5)] + indexer_sl(ser)[Interval(3, 5, "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/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/series/indexing/test_setitem.py b/pandas/tests/series/indexing/test_setitem.py index 8188b2c35c0df..3f340cf6982f3 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)), 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]" @@ -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 From 59005ec1bdf84152887915b2ae93104b4fb7f9e0 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Wed, 30 Mar 2022 20:32:54 +0800 Subject: [PATCH 30/60] test --- pandas/conftest.py | 2 +- pandas/core/arrays/interval.py | 2 +- pandas/tests/arrays/interval/test_interval.py | 2 +- pandas/tests/base/test_conversion.py | 4 ++- pandas/tests/base/test_value_counts.py | 8 ++--- pandas/tests/extension/test_interval.py | 7 ++-- .../frame/constructors/test_from_records.py | 6 +++- pandas/tests/frame/indexing/test_setitem.py | 5 ++- .../tests/frame/methods/test_reset_index.py | 2 +- pandas/tests/frame/methods/test_sort_index.py | 6 +++- pandas/tests/groupby/test_grouping.py | 4 +-- pandas/tests/indexes/interval/test_formats.py | 8 +++-- .../tests/indexes/interval/test_interval.py | 16 ++++----- .../indexes/interval/test_interval_range.py | 6 ++-- .../indexes/interval/test_interval_tree.py | 2 +- .../tests/indexing/interval/test_interval.py | 2 +- pandas/tests/indexing/test_categorical.py | 8 ++--- pandas/tests/reshape/test_cut.py | 35 +++++++++++-------- pandas/tests/reshape/test_pivot.py | 7 ++-- pandas/tests/reshape/test_qcut.py | 18 ++++++---- pandas/tests/scalar/interval/test_interval.py | 34 +++++++++--------- pandas/tests/series/test_constructors.py | 2 +- pandas/tests/util/test_assert_frame_equal.py | 2 +- .../util/test_assert_interval_array_equal.py | 2 +- pandas/tests/util/test_assert_series_equal.py | 2 +- 25 files changed, 112 insertions(+), 80 deletions(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index d362543b76d8f..f0088320188b8 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -562,7 +562,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(), diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index cf2a116bf7afd..72533ff0e013e 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -1136,7 +1136,7 @@ def take( def _validate_listlike(self, value): # list-like of intervals try: - array = IntervalArray(value) + array = IntervalArray(value, inclusive="right") self._check_closed_matches(array, name="value") value_left, value_right = array.left, array.right except TypeError as err: diff --git a/pandas/tests/arrays/interval/test_interval.py b/pandas/tests/arrays/interval/test_interval.py index 60278b48a84fd..812283985b59e 100644 --- a/pandas/tests/arrays/interval/test_interval.py +++ b/pandas/tests/arrays/interval/test_interval.py @@ -135,7 +135,7 @@ 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), inclusive="right") orig = arr.copy() other = arr.set_closed("both") diff --git a/pandas/tests/base/test_conversion.py b/pandas/tests/base/test_conversion.py index 599aaae4d3527..ab3926070978f 100644 --- a/pandas/tests/base/test_conversion.py +++ b/pandas/tests/base/test_conversion.py @@ -291,7 +291,9 @@ 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), + 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/extension/test_interval.py b/pandas/tests/extension/test_interval.py index 0f916cea9d518..8bd545bfd30c0 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -30,7 +30,10 @@ 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, inclusive="right") + for left, right in zip(left_array, right_array) + ] @pytest.fixture @@ -41,7 +44,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 f1e7b18a73173..4ffc81c1a61bd 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_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 9e2391ee69626..907a0a793504c 100644 --- a/pandas/tests/frame/methods/test_sort_index.py +++ b/pandas/tests/frame/methods/test_sort_index.py @@ -726,7 +726,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/groupby/test_grouping.py b/pandas/tests/groupby/test_grouping.py index 3bf6b166acf65..16df253be1d72 100644 --- a/pandas/tests/groupby/test_grouping.py +++ b/pandas/tests/groupby/test_grouping.py @@ -788,13 +788,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, 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/interval/test_formats.py b/pandas/tests/indexes/interval/test_formats.py index a0465cdd9a977..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) diff --git a/pandas/tests/indexes/interval/test_interval.py b/pandas/tests/indexes/interval/test_interval.py index 7ed66a4b8cf26..333b19cdeaaba 100644 --- a/pandas/tests/indexes/interval/test_interval.py +++ b/pandas/tests/indexes/interval/test_interval.py @@ -28,7 +28,7 @@ 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, inclusive="right"): return IntervalIndex.from_breaks(range(11), inclusive=inclusive) @@ -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) @@ -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 @@ -654,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) @@ -666,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 diff --git a/pandas/tests/indexes/interval/test_interval_range.py b/pandas/tests/indexes/interval/test_interval_range.py index cc13690d19afe..5a0a139520ebc 100644 --- a/pandas/tests/indexes/interval/test_interval_range.py +++ b/pandas/tests/indexes/interval/test_interval_range.py @@ -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) tm.assert_index_equal(result, expected) @@ -185,7 +185,7 @@ 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) - 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]) @@ -334,7 +334,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 56aa586501b17..fa794e210c752 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: diff --git a/pandas/tests/indexing/interval/test_interval.py b/pandas/tests/indexing/interval/test_interval.py index db3a569d3925b..a614d38f9ed50 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): diff --git a/pandas/tests/indexing/test_categorical.py b/pandas/tests/indexing/test_categorical.py index eb38edd920082..3d849aabe4b0e 100644 --- a/pandas/tests/indexing/test_categorical.py +++ b/pandas/tests/indexing/test_categorical.py @@ -110,7 +110,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) @@ -122,7 +122,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) @@ -491,13 +491,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/reshape/test_cut.py b/pandas/tests/reshape/test_cut.py index 4f2404dba8cdb..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]) @@ -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", ), ), ], @@ -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(): @@ -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) diff --git a/pandas/tests/reshape/test_pivot.py b/pandas/tests/reshape/test_pivot.py index 31f720b9ec336..f92aecb8dd40e 100644 --- a/pandas/tests/reshape/test_pivot.py +++ b/pandas/tests/reshape/test_pivot.py @@ -299,7 +299,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), @@ -317,7 +317,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 2e6110000a123..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) @@ -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 3d1718ed3077e..878b5e6ec0167 100644 --- a/pandas/tests/scalar/interval/test_interval.py +++ b/pandas/tests/scalar/interval/test_interval.py @@ -13,7 +13,7 @@ @pytest.fixture def interval(): - return Interval(0, 1) + return Interval(0, 1, "right") class TestInterval: @@ -27,7 +27,7 @@ def test_repr(self, interval): assert repr(interval) == "Interval(0, 1, inclusive='right')" assert str(interval) == "(0, 1]" - interval_left = Interval(0, 1, inclusive="left") + interval_left = Interval(0, 1, "left") assert repr(interval_left) == "Interval(0, 1, inclusive='left')" assert str(interval_left) == "[0, 1)" @@ -40,18 +40,18 @@ def test_contains(self, interval): with pytest.raises(TypeError, match=msg): interval in interval - interval_both = Interval(0, 1, inclusive="both") + interval_both = Interval(0, 1, "both") assert 0 in interval_both assert 1 in interval_both - interval_neither = Interval(0, 1, inclusive="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, inclusive="right") - assert Interval(0, 1) != Interval(0, 1, inclusive="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): @@ -152,8 +152,8 @@ def test_construct_errors(self, left, right): Interval(left, right) def test_math_add(self, closed): - interval = Interval(0, 1, inclusive=closed) - expected = Interval(1, 2, inclusive=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, inclusive=closed) - expected = Interval(-1, 0, inclusive=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, inclusive=closed) - expected = Interval(0, 2, inclusive=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, inclusive=closed) - expected = Interval(0, 0.5, inclusive=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, inclusive=closed) - expected = Interval(0, 1, inclusive=closed) + interval = Interval(1, 2, closed) + expected = Interval(0, 1, closed) result = interval // 2 assert result == expected @@ -251,7 +251,7 @@ def test_math_floordiv(self, closed): def test_constructor_errors(self): msg = "invalid option for 'inclusive': foo" with pytest.raises(ValueError, match=msg): - Interval(0, 1, inclusive="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/test_constructors.py b/pandas/tests/series/test_constructors.py index a9c97a830130f..e0b180bf0c6f4 100644 --- a/pandas/tests/series/test_constructors.py +++ b/pandas/tests/series/test_constructors.py @@ -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]" 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 8cc4ade3d7e95..4cf3b5dc91c72 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 From be88126418c3f2d7dd4fba304f76acd4c34febaf Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Thu, 31 Mar 2022 00:55:19 +0800 Subject: [PATCH 31/60] Update v0.20.0.rst --- doc/source/whatsnew/v0.20.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 5588b1abd1169705702c07999da1953e315a9888 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Thu, 31 Mar 2022 00:55:56 +0800 Subject: [PATCH 32/60] Update interval.py --- pandas/core/indexes/interval.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 6eb17e67309dc..417a06e392733 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -263,7 +263,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]') """ @@ -318,7 +318,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]') """ @@ -374,7 +374,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]') """ @@ -534,7 +534,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]') @@ -699,7 +699,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 @@ -712,13 +712,13 @@ 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) From 217348b4979ec2794ddbb83c15712dd0004700f8 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Thu, 31 Mar 2022 00:56:19 +0800 Subject: [PATCH 33/60] Update interval.py --- pandas/core/arrays/interval.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 72533ff0e013e..24ec42b4226cf 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -190,7 +190,7 @@ 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] @@ -449,7 +449,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] @@ -460,7 +460,7 @@ def _from_factorized( def from_breaks( cls: type[IntervalArrayT], breaks, - inclusive="right", + inclusive="both", copy: bool = False, dtype: Dtype | None = None, ) -> IntervalArrayT: @@ -526,7 +526,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] @@ -595,7 +595,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] @@ -606,7 +606,7 @@ def from_arrays( def from_tuples( cls: type[IntervalArrayT], data, - inclusive="right", + inclusive="both", copy: bool = False, dtype: Dtype | None = None, ) -> IntervalArrayT: @@ -1353,7 +1353,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]] @@ -1415,7 +1415,7 @@ def inclusive(self): """\ 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]] @@ -1669,7 +1669,7 @@ 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]] From 7260fcb4f069e39fde8bc79aa0189e19be7b9b02 Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Thu, 31 Mar 2022 00:56:40 +0800 Subject: [PATCH 34/60] Update tile.py --- pandas/core/reshape/tile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/reshape/tile.py b/pandas/core/reshape/tile.py index c2031cf8bf3d1..81435d5010c49 100644 --- a/pandas/core/reshape/tile.py +++ b/pandas/core/reshape/tile.py @@ -230,7 +230,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]] From 707fd4a997826bf1ebd35e965b6abedcc59fa3fb Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Thu, 31 Mar 2022 09:35:58 +0800 Subject: [PATCH 35/60] test --- pandas/tests/arrays/test_array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/arrays/test_array.py b/pandas/tests/arrays/test_array.py index f33eabf5454a2..fa01bed2f292c 100644 --- a/pandas/tests/arrays/test_array.py +++ b/pandas/tests/arrays/test_array.py @@ -135,7 +135,7 @@ ( [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")), From 12e93d5f35240383acc5e897bb57716b8c3405ad Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Thu, 31 Mar 2022 20:31:33 +0800 Subject: [PATCH 36/60] add --- pandas/core/arrays/interval.py | 10 ++++++---- pandas/tests/arrays/interval/test_interval.py | 6 +++--- pandas/tests/base/test_conversion.py | 2 +- pandas/tests/extension/base/setitem.py | 3 ++- pandas/tests/extension/test_interval.py | 3 +-- pandas/tests/frame/test_constructors.py | 5 ++++- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 24ec42b4226cf..c574592cb7725 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -190,7 +190,8 @@ A new ``IntervalArray`` can be constructed directly from an array-like of ``Interval`` objects: - >>> pd.arrays.IntervalArray([pd.Interval(0, 1, "right"), pd.Interval(1, 5, "right")]) + >>> pd.arrays.IntervalArray([pd.Interval(0, 1, "right"), + ... pd.Interval(1, 5, "right")]) [(0, 1], (1, 5]] Length: 2, dtype: interval[int64, right] @@ -1052,7 +1053,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) @@ -1136,7 +1137,7 @@ def take( def _validate_listlike(self, value): # list-like of intervals try: - array = IntervalArray(value, inclusive="right") + array = IntervalArray(value) self._check_closed_matches(array, name="value") value_left, value_right = array.left, array.right except TypeError as err: @@ -1669,7 +1670,8 @@ def repeat( "klass": "IntervalArray", "examples": textwrap.dedent( """\ - >>> intervals = pd.arrays.IntervalArray.from_tuples([(0, 1), (1, 3), (2, 4)], "right") + >>> intervals = pd.arrays.IntervalArray.from_tuples([(0, 1), (1, 3), (2, 4)] + ... , "right") >>> intervals [(0, 1], (1, 3], (2, 4]] diff --git a/pandas/tests/arrays/interval/test_interval.py b/pandas/tests/arrays/interval/test_interval.py index 812283985b59e..297c9b177433d 100644 --- a/pandas/tests/arrays/interval/test_interval.py +++ b/pandas/tests/arrays/interval/test_interval.py @@ -135,7 +135,7 @@ 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), inclusive="right") + arr = IntervalArray.from_breaks(range(4), "right") orig = arr.copy() other = arr.set_closed("both") @@ -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" diff --git a/pandas/tests/base/test_conversion.py b/pandas/tests/base/test_conversion.py index ab3926070978f..3adaddf89cf30 100644 --- a/pandas/tests/base/test_conversion.py +++ b/pandas/tests/base/test_conversion.py @@ -290,7 +290,7 @@ 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]), + IntervalArray.from_breaks([0, 1, 2], "right"), np.array( [pd.Interval(0, 1, "right"), pd.Interval(1, 2, "right")], dtype=object ), diff --git a/pandas/tests/extension/base/setitem.py b/pandas/tests/extension/base/setitem.py index c2db54d832195..6cc5e36431665 100644 --- a/pandas/tests/extension/base/setitem.py +++ b/pandas/tests/extension/base/setitem.py @@ -3,6 +3,7 @@ import pandas as pd import pandas._testing as tm +from pandas.core.arrays import IntervalArray from pandas.tests.extension.base.base import BaseExtensionTests @@ -72,7 +73,7 @@ def test_setitem_empty_indexer(self, data, box_in_series): if box_in_series: data = pd.Series(data) original = data.copy() - data[np.array([], dtype=int)] = [] + data[np.array([], dtype=int)] = IntervalArray([], "right") 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 8bd545bfd30c0..eb307d964d736 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -31,8 +31,7 @@ def make_data(): left_array = np.random.uniform(size=N).cumsum() right_array = left_array + np.random.uniform(size=N) return [ - Interval(left, right, inclusive="right") - for left, right in zip(left_array, right_array) + Interval(left, right, "right") for left, right in zip(left_array, right_array) ] diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index f23c86cf440c7..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"), From 90eea5c7e6fa4404be3b57ded5ca4b3877d705cf Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Thu, 31 Mar 2022 21:24:11 +0800 Subject: [PATCH 37/60] test --- pandas/tests/arrays/interval/test_interval.py | 6 +++--- pandas/tests/arrays/test_array.py | 4 ++-- pandas/tests/extension/base/setitem.py | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pandas/tests/arrays/interval/test_interval.py b/pandas/tests/arrays/interval/test_interval.py index 297c9b177433d..88c7d01925391 100644 --- a/pandas/tests/arrays/interval/test_interval.py +++ b/pandas/tests/arrays/interval/test_interval.py @@ -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) diff --git a/pandas/tests/arrays/test_array.py b/pandas/tests/arrays/test_array.py index fa01bed2f292c..bf7dca9e1d5a0 100644 --- a/pandas/tests/arrays/test_array.py +++ b/pandas/tests/arrays/test_array.py @@ -208,7 +208,7 @@ def test_array_copy(): # interval ( [pd.Interval(0, 1, "right"), pd.Interval(1, 2, "right")], - IntervalArray.from_breaks([0, 1, 2]), + IntervalArray.from_breaks([0, 1, 2], "right"), ), # datetime ( @@ -299,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, inclusive="left"), pd.Interval(1, 2, inclusive="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/extension/base/setitem.py b/pandas/tests/extension/base/setitem.py index 6cc5e36431665..c2db54d832195 100644 --- a/pandas/tests/extension/base/setitem.py +++ b/pandas/tests/extension/base/setitem.py @@ -3,7 +3,6 @@ import pandas as pd import pandas._testing as tm -from pandas.core.arrays import IntervalArray from pandas.tests.extension.base.base import BaseExtensionTests @@ -73,7 +72,7 @@ def test_setitem_empty_indexer(self, data, box_in_series): if box_in_series: data = pd.Series(data) original = data.copy() - data[np.array([], dtype=int)] = IntervalArray([], "right") + data[np.array([], dtype=int)] = [] self.assert_equal(data, original) def test_setitem_sequence_broadcasts(self, data, box_in_series): From 865e9e4871825a478361a3481a5197db7f406c15 Mon Sep 17 00:00:00 2001 From: "chean.wei.khor" Date: Thu, 31 Mar 2022 23:06:07 +0800 Subject: [PATCH 38/60] doc --- pandas/tests/extension/base/setitem.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pandas/tests/extension/base/setitem.py b/pandas/tests/extension/base/setitem.py index c2db54d832195..8a2e41624722b 100644 --- a/pandas/tests/extension/base/setitem.py +++ b/pandas/tests/extension/base/setitem.py @@ -3,6 +3,7 @@ import pandas as pd import pandas._testing as tm +from pandas.core.arrays import IntervalArray from pandas.tests.extension.base.base import BaseExtensionTests @@ -69,10 +70,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): From 59f4681b0b293ab03acd6611282af7dd5dbfe721 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 2 Apr 2022 19:11:31 +0800 Subject: [PATCH 39/60] doc --- pandas/_libs/interval.pyx | 2 +- pandas/core/indexes/interval.py | 8 ++++---- pandas/tests/indexing/interval/test_interval.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 47a5ff00b88da..1c5a5360e0180 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -455,7 +455,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, closed=self.inclusive) return NotImplemented def __sub__(self, y): diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 417a06e392733..bae4cb68eba1b 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -212,7 +212,7 @@ class IntervalIndex(ExtensionIndex): def __new__( cls, data, - inclusive: str = None, + inclusive=None, closed: lib.NoDefault = lib.no_default, dtype: Dtype | None = None, copy: bool = False, @@ -273,7 +273,7 @@ def __new__( def from_breaks( cls, breaks, - inclusive: str = None, + inclusive=None, closed: lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, @@ -329,7 +329,7 @@ def from_arrays( cls, left, right, - inclusive: str = None, + inclusive=None, closed: lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, @@ -384,7 +384,7 @@ def from_arrays( def from_tuples( cls, data, - inclusive: str = None, + inclusive=None, closed: lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, diff --git a/pandas/tests/indexing/interval/test_interval.py b/pandas/tests/indexing/interval/test_interval.py index a614d38f9ed50..7d1f1ef09fc5d 100644 --- a/pandas/tests/indexing/interval/test_interval.py +++ b/pandas/tests/indexing/interval/test_interval.py @@ -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): From e0668d3682cfd9e03f6adea21a73a4f6bef0e7e6 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 2 Apr 2022 20:28:13 +0800 Subject: [PATCH 40/60] doc --- pandas/core/arrays/interval.py | 2 +- pandas/core/dtypes/dtypes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index c574592cb7725..8e55dbd38ca36 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -286,7 +286,7 @@ def _simple_new( cls: type[IntervalArrayT], left, right, - inclusive: str | None = None, + inclusive=None, closed: lib.NoDefault = lib.no_default, copy: bool = False, dtype: Dtype | None = None, diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 2cf0a8e903c54..308cb35195b6a 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1064,7 +1064,7 @@ class IntervalDtype(PandasExtensionDtype): def __new__( cls, subtype=None, - inclusive: str | None = None, + inclusive: str_type | None = None, closed: lib.NoDefault = lib.no_default, ): from pandas.core.dtypes.common import ( From f18fe3acdef25f5d9e7bf0e3f199bb26e0d4312e Mon Sep 17 00:00:00 2001 From: weikhor Date: Sun, 3 Apr 2022 01:10:14 +0800 Subject: [PATCH 41/60] test --- pandas/_libs/interval.pyx | 4 +- .../indexes/interval/test_constructors.py | 62 ++++++++++--------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 1c5a5360e0180..cce7df66dc18e 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -455,7 +455,7 @@ cdef class Interval(IntervalMixin): or PyDelta_Check(other) or is_timedelta64_object(other) ): - return Interval(self.left + other, self.right + other, closed=self.inclusive) + return Interval(self.left + other, self.right + other, inclusive=self.inclusive) return NotImplemented def __sub__(self, y): @@ -479,7 +479,7 @@ cdef class Interval(IntervalMixin): 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.closed) return NotImplemented def __truediv__(self, y): diff --git a/pandas/tests/indexes/interval/test_constructors.py b/pandas/tests/indexes/interval/test_constructors.py index 63fcda8f0f228..b57bcf7abc1e1 100644 --- a/pandas/tests/indexes/interval/test_constructors.py +++ b/pandas/tests/indexes/interval/test_constructors.py @@ -53,7 +53,7 @@ class ConstructorTests: ) def test_constructor(self, constructor, breaks, closed, name): result_kwargs = self.get_kwargs_from_breaks(breaks, closed) - result = constructor(name=name, **result_kwargs) + result = constructor(inclusive=closed, name=name, **result_kwargs) assert result.inclusive == closed assert result.name == name @@ -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) @@ -103,19 +103,20 @@ def test_constructor_pass_closed(self, constructor, breaks): iv_dtype = IntervalDtype(breaks.dtype) - result_kwargs = self.get_kwargs_from_breaks(breaks, inclusive="left") + result_kwargs = self.get_kwargs_from_breaks(breaks) for dtype in (iv_dtype, str(iv_dtype)): with tm.assert_produces_warning(warn): - result = constructor(dtype=dtype, **result_kwargs) + + 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, inclusive=closed) - result = constructor(**result_kwargs) + result_kwargs = self.get_kwargs_from_breaks(breaks) + result = constructor(inclusive=closed, **result_kwargs) expected_subtype = np.float64 expected_values = np.array(breaks[:-1], dtype=object) @@ -137,8 +138,8 @@ 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, inclusive=closed) - result = constructor(**result_kwargs) + result_kwargs = self.get_kwargs_from_breaks(breaks) + result = constructor(inclusive=closed, **result_kwargs) expected_values = np.array([], dtype=object) expected_subtype = getattr(breaks, "dtype", np.int64) @@ -171,7 +172,7 @@ def test_constructor_categorical_valid(self, constructor, cat_constructor): # GH 21243/21253 breaks = np.arange(10, dtype="int64") - expected = IntervalIndex.from_breaks(breaks, inclusive="right") + expected = IntervalIndex.from_breaks(breaks) cat_breaks = cat_constructor(breaks) result_kwargs = self.get_kwargs_from_breaks(cat_breaks) @@ -180,12 +181,12 @@ def test_constructor_categorical_valid(self, constructor, cat_constructor): def test_generic_errors(self, constructor): # filler input data to be used when supplying invalid kwargs - filler = self.get_kwargs_from_breaks(range(10), inclusive="invalid") + filler = self.get_kwargs_from_breaks(range(10)) - # invalid inclusive + # invalid closed msg = "inclusive must be one of 'right', 'left', 'both', 'neither'" with pytest.raises(ValueError, match=msg): - constructor(**filler) + constructor(inclusive="invalid", **filler) # unsupported dtype msg = "dtype must be an IntervalDtype, got int64" @@ -218,12 +219,12 @@ class TestFromArrays(ConstructorTests): def constructor(self): return IntervalIndex.from_arrays - def get_kwargs_from_breaks(self, breaks, inclusive="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 """ - return {"left": breaks[:-1], "right": breaks[1:], "inclusive": inclusive} + return {"left": breaks[:-1], "right": breaks[1:]} def test_constructor_errors(self): # GH 19016: categorical data @@ -267,12 +268,12 @@ class TestFromBreaks(ConstructorTests): def constructor(self): return IntervalIndex.from_breaks - def get_kwargs_from_breaks(self, breaks, inclusive="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 """ - return {"breaks": breaks, "inclusive": inclusive} + return {"breaks": breaks} def test_constructor_errors(self): # GH 19016: categorical data @@ -305,20 +306,20 @@ class TestFromTuples(ConstructorTests): def constructor(self): return IntervalIndex.from_tuples - def get_kwargs_from_breaks(self, breaks, inclusive="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 """ if len(breaks) == 0: - return {"data": breaks, "inclusive": inclusive} + return {"data": breaks} tuples = list(zip(breaks[:-1], breaks[1:])) if isinstance(breaks, (list, tuple)): - return {"data": tuples, "inclusive": inclusive} + return {"data": tuples} elif is_categorical_dtype(breaks): - return {"data": breaks._constructor(tuples), "inclusive": inclusive} - return {"data": com.asarray_tuplesafe(tuples), "inclusive": inclusive} + return {"data": breaks._constructor(tuples)} + return {"data": com.asarray_tuplesafe(tuples)} def test_constructor_errors(self): # non-tuple @@ -355,23 +356,24 @@ class TestClassConstructors(ConstructorTests): def constructor(self, request): return request.param - def get_kwargs_from_breaks(self, breaks, inclusive="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 """ if len(breaks) == 0: - return {"data": breaks, "inclusive": inclusive} + return {"data": breaks} + ivs = [ Interval(left, right, inclusive) if notna(left) else left for left, right in zip(breaks[:-1], breaks[1:]) ] if isinstance(breaks, list): - return {"data": ivs, "inclusive": inclusive} + return {"data": ivs} elif is_categorical_dtype(breaks): - return {"data": breaks._constructor(ivs), "inclusive": inclusive} - return {"data": np.array(ivs, dtype=object), "inclusive": inclusive} + return {"data": breaks._constructor(ivs)} + return {"data": np.array(ivs, dtype=object)} def test_generic_errors(self, constructor): """ @@ -408,7 +410,7 @@ def test_constructor_errors(self, constructor): @pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning") @pytest.mark.parametrize( - "data, inclusive", + "data, closed", [ ([], "both"), ([np.nan, np.nan], "neither"), @@ -426,14 +428,14 @@ def test_constructor_errors(self, constructor): (IntervalIndex.from_breaks(range(5), inclusive="both"), "right"), ], ) - def test_override_inferred_closed(self, constructor, data, inclusive): + def test_override_inferred_closed(self, constructor, data, closed): # GH 19370 if isinstance(data, IntervalIndex): tuples = data.to_tuples() else: tuples = [(iv.left, iv.right) if notna(iv) else iv for iv in data] - expected = IntervalIndex.from_tuples(tuples, inclusive=inclusive) - result = constructor(data, inclusive=inclusive) + expected = IntervalIndex.from_tuples(tuples, inclusive=closed) + result = constructor(data, inclusive=closed) tm.assert_index_equal(result, expected) @pytest.mark.parametrize( From 72d5a0ccb24ee363f4d9aa1b7bd714a3e2d39fa6 Mon Sep 17 00:00:00 2001 From: weikhor Date: Sun, 3 Apr 2022 11:59:29 +0800 Subject: [PATCH 42/60] test --- asv_bench/benchmarks/reshape.py | 4 +++- pandas/_libs/interval.pyx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) 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/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index cce7df66dc18e..99f683f39f333 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -479,7 +479,7 @@ cdef class Interval(IntervalMixin): def __rmul__(self, other): if isinstance(other, numbers.Number): - return Interval(self.left * other, self.right * other, inclusive=self.closed) + return Interval(self.left * other, self.right * other, inclusive=self.inclusive) return NotImplemented def __truediv__(self, y): From 9461b423cbebc1937e2c46ea24088c58948e9ca0 Mon Sep 17 00:00:00 2001 From: weikhor Date: Sun, 3 Apr 2022 20:30:51 +0800 Subject: [PATCH 43/60] test --- pandas/tests/arrays/interval/test_interval.py | 33 +++++++++++++++++++ pandas/tests/dtypes/test_dtypes.py | 15 +++++++++ .../tests/indexes/interval/test_interval.py | 33 +++++++++++++++++++ .../indexes/interval/test_interval_tree.py | 21 ++++++++++++ 4 files changed, 102 insertions(+) diff --git a/pandas/tests/arrays/interval/test_interval.py b/pandas/tests/arrays/interval/test_interval.py index 8289689891c06..29fd52e66d5e6 100644 --- a/pandas/tests/arrays/interval/test_interval.py +++ b/pandas/tests/arrays/interval/test_interval.py @@ -396,3 +396,36 @@ def test_from_arrow_from_raw_struct_array(): 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") + + +@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/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index 58e5f35ad5b24..ebb94091449f0 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -812,6 +812,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/indexes/interval/test_interval.py b/pandas/tests/indexes/interval/test_interval.py index 333b19cdeaaba..3e203aef03a13 100644 --- a/pandas/tests/indexes/interval/test_interval.py +++ b/pandas/tests/indexes/interval/test_interval.py @@ -895,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_tree.py b/pandas/tests/indexes/interval/test_interval_tree.py index fa794e210c752..d03d858086d22 100644 --- a/pandas/tests/indexes/interval/test_interval_tree.py +++ b/pandas/tests/indexes/interval/test_interval_tree.py @@ -189,3 +189,24 @@ 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(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 + IntervalTree(left, right, closed="both", inclusive="both") + + # left, right = np.arange(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 + # IntervalTree(left, right, closed="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(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 + IntervalTree(left, right, closed="both") From 14e9311a04087292675b890a3fa81942563757c7 Mon Sep 17 00:00:00 2001 From: weikhor Date: Sun, 3 Apr 2022 20:46:27 +0800 Subject: [PATCH 44/60] test --- pandas/tests/arrays/interval/test_interval.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pandas/tests/arrays/interval/test_interval.py b/pandas/tests/arrays/interval/test_interval.py index 29fd52e66d5e6..28ffd5b4caf98 100644 --- a/pandas/tests/arrays/interval/test_interval.py +++ b/pandas/tests/arrays/interval/test_interval.py @@ -412,6 +412,20 @@ def test_interval_error_and_warning(): 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 From eb39213ea2703110f40a70a4688d6bbd473604b8 Mon Sep 17 00:00:00 2001 From: weikhor Date: Sun, 3 Apr 2022 20:48:01 +0800 Subject: [PATCH 45/60] test --- pandas/tests/indexes/interval/test_interval_tree.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pandas/tests/indexes/interval/test_interval_tree.py b/pandas/tests/indexes/interval/test_interval_tree.py index d03d858086d22..75d9334f1671d 100644 --- a/pandas/tests/indexes/interval/test_interval_tree.py +++ b/pandas/tests/indexes/interval/test_interval_tree.py @@ -201,9 +201,6 @@ def test_interval_tree_error_and_warning(self): left, right = np.arange(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 IntervalTree(left, right, closed="both", inclusive="both") - # left, right = np.arange(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 - # IntervalTree(left, right, closed="both") - msg = "Argument `closed` is deprecated in favor of `inclusive`" with tm.assert_produces_warning( FutureWarning, match=msg, check_stacklevel=False From eb2e95abbcfb1305419c7a8bdc0c7f6ed3a3dbca Mon Sep 17 00:00:00 2001 From: weikhor Date: Sun, 3 Apr 2022 21:51:32 +0800 Subject: [PATCH 46/60] test --- pandas/tests/indexes/interval/test_interval_tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/indexes/interval/test_interval_tree.py b/pandas/tests/indexes/interval/test_interval_tree.py index 75d9334f1671d..9911c5e255eb1 100644 --- a/pandas/tests/indexes/interval/test_interval_tree.py +++ b/pandas/tests/indexes/interval/test_interval_tree.py @@ -199,11 +199,11 @@ def test_interval_tree_error_and_warning(self): ) with pytest.raises(ValueError, match=msg): left, right = np.arange(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 - IntervalTree(left, right, closed="both", inclusive="both") + IntervalTree(left, right, inclusive="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(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 - IntervalTree(left, right, closed="both") + IntervalTree(left, right, inclusive="both") From 4713022739013c251ad53c9c8cdf433cc05fa802 Mon Sep 17 00:00:00 2001 From: weikhor Date: Sun, 3 Apr 2022 22:51:51 +0800 Subject: [PATCH 47/60] test --- pandas/tests/indexes/interval/test_interval_tree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/indexes/interval/test_interval_tree.py b/pandas/tests/indexes/interval/test_interval_tree.py index 9911c5e255eb1..0ee428325967a 100644 --- a/pandas/tests/indexes/interval/test_interval_tree.py +++ b/pandas/tests/indexes/interval/test_interval_tree.py @@ -199,7 +199,7 @@ def test_interval_tree_error_and_warning(self): ) with pytest.raises(ValueError, match=msg): left, right = np.arange(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 - IntervalTree(left, right, inclusive="both", inclusive="both") + IntervalTree(left, right, inclusive="both") msg = "Argument `closed` is deprecated in favor of `inclusive`" with tm.assert_produces_warning( From fafd961f36aa06d60d6f124256b79a296bd69790 Mon Sep 17 00:00:00 2001 From: weikhor Date: Sun, 3 Apr 2022 23:57:31 +0800 Subject: [PATCH 48/60] test --- pandas/tests/indexes/interval/test_interval_tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/indexes/interval/test_interval_tree.py b/pandas/tests/indexes/interval/test_interval_tree.py index 0ee428325967a..75d9334f1671d 100644 --- a/pandas/tests/indexes/interval/test_interval_tree.py +++ b/pandas/tests/indexes/interval/test_interval_tree.py @@ -199,11 +199,11 @@ def test_interval_tree_error_and_warning(self): ) with pytest.raises(ValueError, match=msg): left, right = np.arange(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 - IntervalTree(left, right, inclusive="both") + 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(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 - IntervalTree(left, right, inclusive="both") + IntervalTree(left, right, closed="both") From 611a27fc75614858a9c87a6ebf63198d7609f00d Mon Sep 17 00:00:00 2001 From: weikhor Date: Mon, 4 Apr 2022 00:31:43 +0800 Subject: [PATCH 49/60] test --- pandas/tests/indexes/interval/test_interval_tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/indexes/interval/test_interval_tree.py b/pandas/tests/indexes/interval/test_interval_tree.py index 75d9334f1671d..345025d63f4b2 100644 --- a/pandas/tests/indexes/interval/test_interval_tree.py +++ b/pandas/tests/indexes/interval/test_interval_tree.py @@ -198,12 +198,12 @@ def test_interval_tree_error_and_warning(self): "be passed if argument `inclusive` is not None" ) with pytest.raises(ValueError, match=msg): - left, right = np.arange(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 + 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(101, dtype="int64"), [np.iinfo(np.int64).max] * 101 + left, right = np.arange(10), [np.iinfo(np.int64).max] * 10 IntervalTree(left, right, closed="both") From 56d7b251e8b0f75dad35718c8f5c3c786ee02af2 Mon Sep 17 00:00:00 2001 From: weikhor Date: Sat, 9 Apr 2022 19:12:44 +0800 Subject: [PATCH 50/60] pre commit --- pandas/tests/indexes/interval/test_interval_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/indexes/interval/test_interval_range.py b/pandas/tests/indexes/interval/test_interval_range.py index bdfb189ec5fa7..255470cf4683e 100644 --- a/pandas/tests/indexes/interval/test_interval_range.py +++ b/pandas/tests/indexes/interval/test_interval_range.py @@ -188,7 +188,7 @@ def test_linspace_dst_transition(self, start, mid, end): # 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], "right") - + tm.assert_index_equal(result, expected) @pytest.mark.parametrize("freq", [2, 2.0]) From d4c221291f245213ae22cf44b3597c4d070cc8f8 Mon Sep 17 00:00:00 2001 From: weikhor Date: Fri, 29 Apr 2022 23:24:58 +0800 Subject: [PATCH 51/60] add --- pandas/core/arrays/arrow/_arrow_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandas/core/arrays/arrow/_arrow_utils.py b/pandas/core/arrays/arrow/_arrow_utils.py index 41941c9972feb..20406611b84ca 100644 --- a/pandas/core/arrays/arrow/_arrow_utils.py +++ b/pandas/core/arrays/arrow/_arrow_utils.py @@ -6,9 +6,7 @@ import numpy as np import pyarrow - from pandas._libs import lib - from pandas.errors import PerformanceWarning from pandas.util._exceptions import find_stack_level From c0653ee3adb09515c1d65bce81b2358efd776ea2 Mon Sep 17 00:00:00 2001 From: weikhor Date: Mon, 16 May 2022 15:46:35 +0800 Subject: [PATCH 52/60] add helper function --- doc/source/whatsnew/v1.5.0.rst | 12 +-- pandas/_libs/interval.pyx | 50 +++++----- pandas/_libs/intervaltree.pxi.in | 26 +---- pandas/core/arrays/arrow/_arrow_utils.py | 22 +---- pandas/core/arrays/interval.py | 44 +-------- pandas/core/dtypes/dtypes.py | 27 +----- pandas/core/indexes/datetimes.py | 23 +---- pandas/core/indexes/interval.py | 115 ++--------------------- pandas/tests/dtypes/test_dtypes.py | 19 ++++ 9 files changed, 82 insertions(+), 256 deletions(-) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 616f2c9bc91ba..788f3d5a78c5b 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -455,12 +455,12 @@ Other Deprecations - Deprecated passing arguments as positional in :meth:`DataFrame.any` and :meth:`Series.any` (:issue:`44802`) - Deprecated the ``closed`` argument in :meth:`interval_range` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) - Deprecated the methods :meth:`DataFrame.mad`, :meth:`Series.mad`, and the corresponding groupby methods (:issue:`11787`) -- Deprecated the ``closed`` argument in :meth:`Interval.__init__` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) -- Deprecated the ``closed`` argument in :meth:`IntervalIndex.__new__`, :meth:`IntervalIndex.from_breaks`, :meth:`IntervalIndex.from_arrays` and :meth:`IntervalIndex.from_tuples` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) -- Deprecated the ``closed`` argument in :meth:`IntervalDtype.__new__` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) -- Deprecated the ``closed`` argument in :meth:`IntervalArray.__new__` and :meth:`IntervalArray._simple_new`, in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) -- Deprecated the ``closed`` argument in :meth:`intervaltree.__init__` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) -- Deprecated the ``closed`` argument in :meth:`ArrowInterval.__init__` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) +- 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`) - .. --------------------------------------------------------------------------- diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 82057a958b001..8dfbf012d9dee 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -213,6 +213,32 @@ cdef bint _interval_like(other): and hasattr(other, 'right') and hasattr(other, 'inclusive')) +def warning_interval(inclusive: str | None = None, closed: lib.NoDefault = lib.no_default): + """ + warning in interval class for variable inclusive and closed + """ + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + 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): """ @@ -335,27 +361,9 @@ cdef class Interval(IntervalMixin): self._validate_endpoint(left) self._validate_endpoint(right) - if inclusive is not None and not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - 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" if inclusive not in VALID_CLOSED: diff --git a/pandas/_libs/intervaltree.pxi.in b/pandas/_libs/intervaltree.pxi.in index 2a4bd9a3c9a2c..bd48085c3f494 100644 --- a/pandas/_libs/intervaltree.pxi.in +++ b/pandas/_libs/intervaltree.pxi.in @@ -8,6 +8,8 @@ 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 @@ -64,27 +66,9 @@ cdef class IntervalTree(IntervalMixin): to brute-force search. Tune this parameter to optimize query performance. """ - if inclusive is not None and not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - 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" if inclusive not in ['left', 'right', 'both', 'neither']: diff --git a/pandas/core/arrays/arrow/_arrow_utils.py b/pandas/core/arrays/arrow/_arrow_utils.py index 20406611b84ca..50403bde6566a 100644 --- a/pandas/core/arrays/arrow/_arrow_utils.py +++ b/pandas/core/arrays/arrow/_arrow_utils.py @@ -7,6 +7,7 @@ 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 @@ -112,26 +113,7 @@ def __init__( ) -> None: # attributes need to be set first before calling # super init (as that calls serialize) - if inclusive is not None and not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - 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'" - ) + inclusive, closed = warning_interval(inclusive, closed) assert inclusive in VALID_CLOSED self._closed = inclusive if not isinstance(subtype, pyarrow.DataType): diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index f115686d2474d..2e7ddb7fa3d89 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -14,7 +14,6 @@ cast, overload, ) -import warnings import numpy as np @@ -26,6 +25,7 @@ Interval, IntervalMixin, intervals_to_interval_bounds, + warning_interval, ) from pandas._libs.missing import NA from pandas._typing import ( @@ -225,26 +225,7 @@ def __new__( copy: bool = False, verify_integrity: bool = True, ): - if inclusive is not None and not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - 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'" - ) + inclusive, closed = warning_interval(inclusive, closed) data = extract_array(data, extract_numpy=True) @@ -294,26 +275,7 @@ def _simple_new( ) -> IntervalArrayT: result = IntervalMixin.__new__(cls) - if inclusive is not None and not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - 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'" - ) + inclusive, closed = warning_interval(inclusive, closed) if inclusive is None and isinstance(dtype, IntervalDtype): inclusive = dtype.inclusive diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 87bd441b1abaf..624b59ceeed72 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -10,7 +10,6 @@ MutableMapping, cast, ) -import warnings import numpy as np import pytz @@ -19,7 +18,10 @@ lib, missing as libmissing, ) -from pandas._libs.interval import Interval +from pandas._libs.interval import ( + Interval, + warning_interval, +) from pandas._libs.properties import cache_readonly from pandas._libs.tslibs import ( BaseOffset, @@ -1074,26 +1076,7 @@ def __new__( pandas_dtype, ) - if inclusive is not None and not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - 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'" - ) + inclusive, closed = warning_interval(inclusive, closed) if inclusive is not None and inclusive not in { "right", diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 3954cb28c2aca..f981d5faa3cfa 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -24,6 +24,7 @@ index as libindex, lib, ) +from pandas._libs.interval import warning_interval from pandas._libs.tslibs import ( Resolution, timezones, @@ -1042,26 +1043,8 @@ def date_range( DatetimeIndex(['2017-01-02', '2017-01-03', '2017-01-04'], dtype='datetime64[ns]', freq='D') """ - if inclusive is not None and not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed" - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - warnings.warn( - "Argument `closed` is deprecated in favor of `inclusive`.", - FutureWarning, - stacklevel=find_stack_level(), - ) - if closed is None: - inclusive = "both" - elif closed in ("left", "right"): - inclusive = closed - else: - raise ValueError( - "Argument `closed` has to be either 'left', 'right' or None" - ) - elif inclusive is None: + inclusive, closed = warning_interval(inclusive, closed) + if inclusive is None: inclusive = "both" if freq is None and com.any_none(periods, start, end): diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index c30c6fd17860a..79e2b8f27210f 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, @@ -220,26 +220,7 @@ def __new__( verify_integrity: bool = True, ) -> IntervalIndex: - if inclusive is not None and not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - 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'" - ) + inclusive, closed = warning_interval(inclusive, closed) name = maybe_extract_name(name, data, cls) @@ -280,27 +261,8 @@ def from_breaks( dtype: Dtype | None = None, ) -> IntervalIndex: - if inclusive is not None and not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - 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" with rewrite_exception("IntervalArray", cls.__name__): @@ -336,27 +298,8 @@ def from_arrays( dtype: Dtype | None = None, ) -> IntervalIndex: - if inclusive is not None and not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - 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" with rewrite_exception("IntervalArray", cls.__name__): @@ -391,27 +334,8 @@ def from_tuples( dtype: Dtype | None = None, ) -> IntervalIndex: - if inclusive is not None and not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - 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" with rewrite_exception("IntervalArray", cls.__name__): @@ -1153,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 not isinstance(closed, lib.NoDefault): - raise ValueError( - "Deprecated argument `closed` cannot be passed " - "if argument `inclusive` is not None" - ) - elif not isinstance(closed, lib.NoDefault): - 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) diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index ebb94091449f0..a39f4f6200261 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -1,3 +1,4 @@ +import pickle import re import numpy as np @@ -572,6 +573,15 @@ def test_construction(self, subtype): 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")] ) @@ -827,6 +837,15 @@ def test_interval_dtype_error_and_warning(self): ): IntervalDtype("int64", closed="right") + def test_interval_dtype_with_pickle(self): + i = IntervalDtype(subtype="int64", inclusive="right") + dumps_i = pickle.dumps(i) + loads_i = pickle.loads(dumps_i) + + assert loads_i.subtype == np.dtype("int64") + assert loads_i.inclusive == "right" + assert is_interval_dtype(loads_i) + class TestCategoricalDtypeParametrized: @pytest.mark.parametrize( From c32384ee439243568ff19ea81c4e57cb2b97a721 Mon Sep 17 00:00:00 2001 From: weikhor Date: Mon, 16 May 2022 22:35:41 +0800 Subject: [PATCH 53/60] interval --- pandas/_libs/interval.pyi | 4 ++++ pandas/_libs/interval.pyx | 2 ++ pandas/core/indexes/datetimes.py | 23 ++++++++++++++++++++--- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/interval.pyi b/pandas/_libs/interval.pyi index 22429a3f14834..81fa9590cf551 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,6 +55,8 @@ 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: ... @@ -67,6 +70,7 @@ class Interval(IntervalMixin, Generic[_OrderableT]): self, left: _OrderableT, right: _OrderableT, + inclusive: IntervalClosedType = ..., closed: IntervalClosedType = ..., ) -> None: ... def __hash__(self) -> int: ... diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 8dfbf012d9dee..284d7bf498e4e 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -213,6 +213,8 @@ cdef bint _interval_like(other): and hasattr(other, 'right') and hasattr(other, 'inclusive')) +@cython.wraparound(False) +@cython.boundscheck(False) def warning_interval(inclusive: str | None = None, closed: lib.NoDefault = lib.no_default): """ warning in interval class for variable inclusive and closed diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index f981d5faa3cfa..3954cb28c2aca 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -24,7 +24,6 @@ index as libindex, lib, ) -from pandas._libs.interval import warning_interval from pandas._libs.tslibs import ( Resolution, timezones, @@ -1043,8 +1042,26 @@ def date_range( DatetimeIndex(['2017-01-02', '2017-01-03', '2017-01-04'], dtype='datetime64[ns]', freq='D') """ - inclusive, closed = warning_interval(inclusive, closed) - if inclusive is None: + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed" + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + warnings.warn( + "Argument `closed` is deprecated in favor of `inclusive`.", + FutureWarning, + stacklevel=find_stack_level(), + ) + if closed is None: + inclusive = "both" + elif closed in ("left", "right"): + inclusive = closed + else: + raise ValueError( + "Argument `closed` has to be either 'left', 'right' or None" + ) + elif inclusive is None: inclusive = "both" if freq is None and com.any_none(periods, start, end): From eab8b58e052d65e54ebeb5511eb406980dd7478e Mon Sep 17 00:00:00 2001 From: weikhor Date: Thu, 19 May 2022 01:40:53 +0800 Subject: [PATCH 54/60] update warning_interval --- pandas/_libs/interval.pyi | 4 +++- pandas/_libs/interval.pyx | 6 ++---- pandas/_libs/intervaltree.pxi.in | 4 ++-- pandas/core/arrays/arrow/_arrow_utils.py | 4 ++-- pandas/core/arrays/interval.py | 6 +++--- pandas/core/dtypes/dtypes.py | 4 ++-- pandas/core/indexes/interval.py | 12 ++++++------ 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pandas/_libs/interval.pyi b/pandas/_libs/interval.pyi index 81fa9590cf551..d177e597478d9 100644 --- a/pandas/_libs/interval.pyi +++ b/pandas/_libs/interval.pyi @@ -55,7 +55,9 @@ 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]: ... +def _warning_interval( + inclusive, closed +) -> tuple[IntervalClosedType, lib.NoDefault]: ... class Interval(IntervalMixin, Generic[_OrderableT]): @property diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 284d7bf498e4e..afbb81a48541c 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -213,9 +213,7 @@ cdef bint _interval_like(other): and hasattr(other, 'right') and hasattr(other, 'inclusive')) -@cython.wraparound(False) -@cython.boundscheck(False) -def warning_interval(inclusive: str | None = None, closed: lib.NoDefault = lib.no_default): +def _warning_interval(inclusive: str | None = None, closed: lib.NoDefault = lib.no_default): """ warning in interval class for variable inclusive and closed """ @@ -363,7 +361,7 @@ cdef class Interval(IntervalMixin): self._validate_endpoint(left) self._validate_endpoint(right) - inclusive, closed = warning_interval(inclusive, closed) + inclusive, closed = _warning_interval(inclusive, closed) if inclusive is None: inclusive = "both" diff --git a/pandas/_libs/intervaltree.pxi.in b/pandas/_libs/intervaltree.pxi.in index bd48085c3f494..7722e9d01a3d2 100644 --- a/pandas/_libs/intervaltree.pxi.in +++ b/pandas/_libs/intervaltree.pxi.in @@ -8,7 +8,7 @@ import warnings from pandas._libs import lib from pandas._libs.algos import is_monotonic -from pandas._libs.interval import warning_interval +from pandas._libs.interval import _warning_interval ctypedef fused int_scalar_t: int64_t @@ -66,7 +66,7 @@ cdef class IntervalTree(IntervalMixin): to brute-force search. Tune this parameter to optimize query performance. """ - inclusive, closed = warning_interval(inclusive, closed) + inclusive, closed = _warning_interval(inclusive, closed) if inclusive is None: inclusive = "both" diff --git a/pandas/core/arrays/arrow/_arrow_utils.py b/pandas/core/arrays/arrow/_arrow_utils.py index 50403bde6566a..8e451bbf26f86 100644 --- a/pandas/core/arrays/arrow/_arrow_utils.py +++ b/pandas/core/arrays/arrow/_arrow_utils.py @@ -7,7 +7,7 @@ import pyarrow from pandas._libs import lib -from pandas._libs.interval import warning_interval +from pandas._libs.interval import _warning_interval from pandas.errors import PerformanceWarning from pandas.util._exceptions import find_stack_level @@ -113,7 +113,7 @@ def __init__( ) -> None: # attributes need to be set first before calling # super init (as that calls serialize) - inclusive, closed = warning_interval(inclusive, closed) + inclusive, closed = _warning_interval(inclusive, closed) assert inclusive in VALID_CLOSED self._closed = inclusive if not isinstance(subtype, pyarrow.DataType): diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 2e7ddb7fa3d89..eb838126a3052 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -24,8 +24,8 @@ VALID_CLOSED, Interval, IntervalMixin, + _warning_interval, intervals_to_interval_bounds, - warning_interval, ) from pandas._libs.missing import NA from pandas._typing import ( @@ -225,7 +225,7 @@ def __new__( copy: bool = False, verify_integrity: bool = True, ): - inclusive, closed = warning_interval(inclusive, closed) + inclusive, closed = _warning_interval(inclusive, closed) data = extract_array(data, extract_numpy=True) @@ -275,7 +275,7 @@ def _simple_new( ) -> IntervalArrayT: result = IntervalMixin.__new__(cls) - inclusive, closed = warning_interval(inclusive, closed) + inclusive, closed = _warning_interval(inclusive, closed) if inclusive is None and isinstance(dtype, IntervalDtype): inclusive = dtype.inclusive diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 624b59ceeed72..177b4a3c20a0b 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -20,7 +20,7 @@ ) from pandas._libs.interval import ( Interval, - warning_interval, + _warning_interval, ) from pandas._libs.properties import cache_readonly from pandas._libs.tslibs import ( @@ -1076,7 +1076,7 @@ def __new__( pandas_dtype, ) - inclusive, closed = warning_interval(inclusive, closed) + inclusive, closed = _warning_interval(inclusive, closed) if inclusive is not None and inclusive not in { "right", diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 75d7e332aefe1..253366ae8ffd3 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -19,7 +19,7 @@ Interval, IntervalMixin, IntervalTree, - warning_interval, + _warning_interval, ) from pandas._libs.tslibs import ( BaseOffset, @@ -220,7 +220,7 @@ def __new__( verify_integrity: bool = True, ) -> IntervalIndex: - inclusive, closed = warning_interval(inclusive, closed) + inclusive, closed = _warning_interval(inclusive, closed) name = maybe_extract_name(name, data, cls) @@ -261,7 +261,7 @@ def from_breaks( dtype: Dtype | None = None, ) -> IntervalIndex: - inclusive, closed = warning_interval(inclusive, closed) + inclusive, closed = _warning_interval(inclusive, closed) if inclusive is None: inclusive = "both" @@ -298,7 +298,7 @@ def from_arrays( dtype: Dtype | None = None, ) -> IntervalIndex: - inclusive, closed = warning_interval(inclusive, closed) + inclusive, closed = _warning_interval(inclusive, closed) if inclusive is None: inclusive = "both" @@ -334,7 +334,7 @@ def from_tuples( dtype: Dtype | None = None, ) -> IntervalIndex: - inclusive, closed = warning_interval(inclusive, closed) + inclusive, closed = _warning_interval(inclusive, closed) if inclusive is None: inclusive = "both" @@ -1077,7 +1077,7 @@ def interval_range( IntervalIndex([[1, 2], [2, 3], [3, 4], [4, 5]], dtype='interval[int64, both]') """ - inclusive, closed = warning_interval(inclusive, closed) + inclusive, closed = _warning_interval(inclusive, closed) if inclusive is None: inclusive = "both" From fe62e502a8c552994bc420e223e789119fd0ef74 Mon Sep 17 00:00:00 2001 From: weikhor Date: Thu, 19 May 2022 23:37:31 +0800 Subject: [PATCH 55/60] pickle --- pandas/core/dtypes/dtypes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 177b4a3c20a0b..7f99a701cd621 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1236,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 "inclusive" key - self._closed = state.pop("inclusive", None) + # backward-compat older pickles won't have "closed" key + self._closed = state.pop("closed", None) @classmethod def is_dtype(cls, dtype: object) -> bool: From a73ec915bc48611e272fac5231f27dac6edac91f Mon Sep 17 00:00:00 2001 From: Khor Chean Wei Date: Fri, 20 May 2022 01:38:37 +0800 Subject: [PATCH 56/60] Update dtypes.py --- pandas/core/dtypes/dtypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 7f99a701cd621..d664129c3973a 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1236,8 +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: From 757cabf41ef57158ccaa6037f5ef0f38d06b6edd Mon Sep 17 00:00:00 2001 From: weikhor Date: Wed, 25 May 2022 00:50:28 +0800 Subject: [PATCH 57/60] pickle --- pandas/_libs/tslibs/timestamps.pyx | 2 +- .../1.4.2/1.4.2_x86_64_linux_3.9.7.pickle | Bin 0 -> 126123 bytes ...g7bdde2460b.dirty_x86_64_linux_3.9.7.pickle | Bin 0 -> 126144 bytes 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 pandas/tests/io/data/legacy_pickle/1.4.2/1.4.2_x86_64_linux_3.9.7.pickle create mode 100644 pandas/tests/io/data/legacy_pickle/1.4.2/1.5.0.dev0+824.g7bdde2460b.dirty_x86_64_linux_3.9.7.pickle diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index abdb4aebb625f..a1dc70397fbb9 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -148,7 +148,7 @@ cdef inline _Timestamp create_timestamp_from_ts( return ts_base -def _unpickle_timestamp(value, freq, tz, reso): +def _unpickle_timestamp(value, freq, tz, reso=NPY_FR_ns): # GH#41949 dont warn on unpickle if we have a freq if reso == NPY_FR_ns: ts = Timestamp(value, tz=tz) diff --git a/pandas/tests/io/data/legacy_pickle/1.4.2/1.4.2_x86_64_linux_3.9.7.pickle b/pandas/tests/io/data/legacy_pickle/1.4.2/1.4.2_x86_64_linux_3.9.7.pickle new file mode 100644 index 0000000000000000000000000000000000000000..403ccf99c06d1eba93edae53e6d5bb9a6f5b80e4 GIT binary patch literal 126123 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 EKOfRpr2qf` literal 0 HcmV?d00001 diff --git a/pandas/tests/io/data/legacy_pickle/1.4.2/1.5.0.dev0+824.g7bdde2460b.dirty_x86_64_linux_3.9.7.pickle b/pandas/tests/io/data/legacy_pickle/1.4.2/1.5.0.dev0+824.g7bdde2460b.dirty_x86_64_linux_3.9.7.pickle new file mode 100644 index 0000000000000000000000000000000000000000..6010445ab4210dae23d4e9b1afc939383ffac231 GIT binary patch literal 126144 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 Date: Wed, 25 May 2022 00:52:05 +0800 Subject: [PATCH 58/60] pickle --- pandas/_libs/tslibs/timestamps.pyx | 2 +- .../1.4.2/1.4.2_x86_64_linux_3.9.7.pickle | Bin 126123 -> 0 bytes ...g7bdde2460b.dirty_x86_64_linux_3.9.7.pickle | Bin 126144 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 pandas/tests/io/data/legacy_pickle/1.4.2/1.4.2_x86_64_linux_3.9.7.pickle delete mode 100644 pandas/tests/io/data/legacy_pickle/1.4.2/1.5.0.dev0+824.g7bdde2460b.dirty_x86_64_linux_3.9.7.pickle diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index a1dc70397fbb9..abdb4aebb625f 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -148,7 +148,7 @@ cdef inline _Timestamp create_timestamp_from_ts( return ts_base -def _unpickle_timestamp(value, freq, tz, reso=NPY_FR_ns): +def _unpickle_timestamp(value, freq, tz, reso): # GH#41949 dont warn on unpickle if we have a freq if reso == NPY_FR_ns: ts = Timestamp(value, tz=tz) diff --git a/pandas/tests/io/data/legacy_pickle/1.4.2/1.4.2_x86_64_linux_3.9.7.pickle b/pandas/tests/io/data/legacy_pickle/1.4.2/1.4.2_x86_64_linux_3.9.7.pickle deleted file mode 100644 index 403ccf99c06d1eba93edae53e6d5bb9a6f5b80e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126123 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 EKOfRpr2qf` diff --git a/pandas/tests/io/data/legacy_pickle/1.4.2/1.5.0.dev0+824.g7bdde2460b.dirty_x86_64_linux_3.9.7.pickle b/pandas/tests/io/data/legacy_pickle/1.4.2/1.5.0.dev0+824.g7bdde2460b.dirty_x86_64_linux_3.9.7.pickle deleted file mode 100644 index 6010445ab4210dae23d4e9b1afc939383ffac231..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126144 zcmdqq1zZ%(IH@b z1o>4A3iWC2Zhh5k*@<%tvnl1bpZ4`@?-$|X?G@o?HaU8R__gz_9wJ|p+nZATY|C)3 zkN|zty1Fr&>Zf88;uY*?_AuE-M0$lsn%xwWO+;j9nAy!%|G`iGphopI*EM^|V@MVf z6&%*VdbYt)L6L!8;o)8#%%*sr;eI}$ArX<`Q9hCSA=p|rG@Ihc*s*LfLhiVa*;Vy6 z_mlf_c2nJK`TFGp zn8zpO_^G^AdwkQ)EH{@&Y;ZCpl_zOP?qNvbrWjJX*&0&CoKF|C$u7T3fx<2Ytcm0$ zBh+k2V6yYp<3GaO!t%0RTd$xf{f+D+{X&QS zGbqqI!X;9sy-r}TpS*g4!(?_y;u#eZ7U+R&>;pP}A zbBRgw4-GX}CCe&)f0AQk;xO!M9Jc$*I24gGGyjl>OzD1BXviGvXE_U5Hd)G;O1uzL zfEdP@;(z~|rBJctbyE^{YK<*RNg$&rt*2*LhxT9N$)i#Qy^N^n<|?Qh z$|csJeEE%*kI4_zSkt^gd=EW)8)UwA|C*>aKjdq9aIQjLwz2?{|02zO^{o5jCE_Tr zm;Y#2(XO=>W!If^b|(`nO0tfj4Rn3u+RV^hK4Hu=UEz|q!YOH;&0B})PO07SlJ4WH z_wt;UmW}AvNq3M<(tW2B>qjUH3R_SAaKAQYQ<`t$lpGl9?;qhO6V{|vlI5{1i_~bn z%#P?MPfivcl;1x;&t{fmw&wAe3tN!oHMBunw8JGbwqy-ifxNIL(+~U5toum$tBeq{ugG?x)_wVzY<)s~WreAq z#t*yGD}BA{De$$bH+244)gv>Ss;ZW~|7<5lCwWrQuC|J*s6yY$&*Tsh8Xg%M?&}w> zH)ixkjrABl813Y?e!sp3^kd5N$4!ew<@H}@ZCX6iBkhSLy<#HpjUl<=!?%%Y6!P~Q zl3NbL@{XU;LB8^<`1x{be;EPBG;*gN#&;BwdW&7^uF_~09)-UZ_H@M6o_qi55AX7sr_0>y2Qie~L}L ztgxoA=Itf>JMyuk+0Z+7(wgE}u25OcM@Rjn+Le1C`)fk6T>V{Sf8nPM`B)M2HCZ}+ zlPtZzCX4o`MY-=ErAn+`gq|wDtK0N!_8+z;WQXU^S`&JyBqL&zhhFFZw&z2wXI%14 zgsu7-Vc+&-eqHy;{+nl{OywUMl)veed=qEJKJpiv62>$n>o@%_3))151j^2~-flAq z`P)o3VI6dYXPDl!HaRw^m#?yy?5u`|m}^W~_xZEz#A-F&>&rNiow1+fv;R?oWc0dF zB86VtNsvo_pCEQ&vV{=(AM}x? zep^6Lwy-x-+}f7i{@CKSrX6p`q?=*0$+1q998T~HuN1Er?EuP*$?<4>JyjdVy zD$ea`KK@Q?W|w_uA1@z+pX^2(oQ)Y|n>FIEGJgX3iLZrt3jNt%72;8H7${sHJkd*R z&Eyoj*J6Fcdni{Upn{gpAA|<4?HR z_O&!(WalGx`1SaRb*o>!pQya`ed{=irHd}CC8(RQrlPFJ|G6^I8=scS;A_d16hyXdHX3YeDHArSTG1(Q? zlhvBI|G0boDvgQ6nE3kDbK*^P`zG)Fko6!YgJL~4S<+eYWleN5| zESE8+x!J^yu}Ev5?Vs94Ha}htUt5~rTo}I|vHX2*x&32q`N!z~+iuUlkXd3bw(mQX zKhG?Ee;>2ID`=byvASvhltf>5G)(4f<8b+4O&D3s2ZqZ5UAlGuw2v=MV8aKKZKNUG z&(Cb_ep z$m#DIQ(;pS{H|U}THd;0N=%CxF*9bx?C6ZSFps4(ZpoG36a;;KpKZRR)YtmyhYsIA zv|)c6ktV(0YB z=npsbhX?ZEXPx|5&AcyN^_VXE&;5te@b3=kjyxo1ljd5vzS%ud0(~?>|M6F&5wQ+* z%RtXmueBt~Z;XAQ-!-rQl@`;trNrMPZTy&|EhvYD{=ZDyyRzl_o9y8+}c;9uj`u%6^D}8AF-|Fkg!57P{ zm}T&#rYw$L{9=q>pUVB&sDLa(E#do)$lpwH#>`llY~&z@pV{zt)1Cj}knn$Lg83Iy zq((;>QF1;qb?nitnEA->MhCy00h($|CO;ahahpot(`J*sduT|c;mf$7J`D8pQNfs$ zvmvQ$(GKIFJvyL%#CnK}@i0Dq&Dczh8#I}Jo|)oR(HoqUW}GNtSJA=h2Wqs7xDEA zk}I8LrulBgn{^WV%NBApKSBMN&A-b#->yetF#6}8{BcZLkBH!}5%E7+1!7zxmp;Ub zG^KX)l+*Q?o3bW<6+OnDB&~i|Uma1tjh9JZ6*2T+hkW+ZD>;WB(^o^}?>jFluC)8L zZ7JhRkCZ*XK7m5VOI~xEO?K{qAyKmQkfY@3e^|9)vb8SU`=Pz%_@zf>vbPKu%XMns zt(=LuO258sUBn}^lV7;pJSNJGd*un3;(!0LEKf}~L4N*nM(qD^>1rDu7-0CL!}z!I zw4+QZJ$!Rb+W7>9M#!m4lZ|(1q(QFZvo5TOGR29IQ?Fi@^?c4I%N1Z*4Q4j36Fe?7s{>$SXuuh0ruh1phFW zAA3W&!bAV3Z}^+Rtq8dWB+T>cDqUHu|9)CB;g{=3PMVrB#IEHibt1xK()zhrp2_+3 zdX}d#g$|SC@jb0ud0MVHxeD-`=^X2Oa@CGy)+gq&b4&P>NepW#9#}Zm`k0?C&D4t+ zC0AzY|)>#%C{9_!m1#uiRn@G2>iaZ}f9#dRT|G=OiPs_oZt9E;1J;EsOsGrVy$4Xu~ zOK4r|Wz3-ODP|F>F^B%lI`NnA50(j-;&_wAWG`UEPjc9wHv$t{Q~U21EJo`Cu7}g_ zt?*J#QKVneVyJF#g=d#^HyrDm_mwoRZ<@}!Z&I8McrC3uH+YrqsFK3=jr8XJQT22S zx5zzBH;m4t}zQlf5kDW9Hor+Kx_YBdFVOUU%P%sM+>72rC@!~*75=>Z)OY{KndcYd&Oe<0 zYI!1V|9r~&!Rjq2y}J0n{FJ{N^|FkY{P8Kv`i*iVYs~5X4^msNE#&}1wSTMa`Rmkg zW+^({_Ec!`E#4Z(Y1G9Y?*(uLr%2q6fX}uLs%nw}gb42xa@q z669SUmLNxGEg>l;!{nF(Ef=r+ITg=SV;W40=`cOUoO(u{XTr>w1+!u{%#JzG8FOMT z%#C?4FS=kpj43?|@Vp=v!opYti()a1sbfp{5Fg=Ve1cE$89v7s_!3{?YkY%m@g2U$5BL#3;b*k;cjV8iYJEY! zS4$5X+Mq4!_mb%$4%(xBN0%NPF)qf#_?Q3_V$98XdUHn)i7^Q##blTqQ(#I=g{e`$ z8B7oQ?O=LHhv_i`X2eXG8TFgP^kC^ANwe`hJLW)V%!#=$H|D{-=z{q$KNi4(SO^Pa z5iE+uusD{$l2{5$V;L-q<*+4UKm9R2a!KxV3>(iG_>Y)a@VNKL;7}J9X>Nln7 zp*GgRx>yhEV*_l6`mJhu(5KY(px>~jho;yJn_~-XiJsUBz0ezd&=>vC9|O>UfoR0m z7=*zXf}t3OZ7>`oFcPD%E$UqtJ+#LT*b((R!Sv7>yI@!BhTX9T_QYP;8~b2i?1%j^ z8VBG&G~pl|j6-lJ4#VL%0?jxQN8xB3gJW?Vj>ic&5hvkfoPtwv8cxRVx%J$As3*a)Jra4e3)@i+k| z;v}4mQ*bIy!|6B!XW}fJjdO4=&cpe*02ksST#QR_DK5k1xB^$=DqM|ga4oLG^|%2y z;wIdTTW~9G!|k{Ocj7MGjeBq}?!*0f01x6JJd8*1C?3P(cmhx2DLjp5@GPFg^LPO- z;w8L{SMVxc!|QkhZ{jVyjd$=a-oyL&03YHbe2h=_xJ%n z;wSu!F{>U`&01ax)X)ZP(GKIFJvyKx#>IFT9}{3gOoUFD7?WU9OoquZ1;%_aPb!|L z#x!V|#+Ij+j_2ty17^fbm>IKRR?LRkF$X$hPRxb5F%RZN7tDwGu>cmtLRc7!U{NfF z#jymI#8Oxq%V1e7hvl&XRzz2R1EauqL{r2iC&cSO@E3J*l;aE!o6jKa3q z4%=e~?1-JPGj_qQ*bTd55A2D(us8O>zSs}@V>AxHfoQ@(I2ecEP#lKCaRi!iB#y$- zI0nb!I2?}?a3W5^$v6e4;xwF&GjJx(!r3?n=i)q^j|*@iF2cpQ1efA6T#hSnC9cBN zxCYnaI$Vz%a3gNQ&A0`(;x^olJ8&oN!rizB_u@X>j|cD|9>T+T1drk|JdP*uB%Z?4 zcm~hnIXsUS@FHHq%XkH^;x)XEH}EFj!rQ1%%*QN*{^f+eb;eyT$`35~n{}-fkHpdI zVO0@|CRbnC%4JA#Voc&I%q*2l; z>6G+J1|_4CNy)5aQL-x8ltuM|)UDutB7N)e@~QcNkX zlu$}4rIgZ28KtaJPARWcP%0{}N+qSTQbpcqSxu>~)R4;;Ybx%Fhf+(at<+KKD)p55 zN&}^#(nx8nG*Ox=&6MU!3#FyvskBnO6mP{x@m2g3el()(|<-PJj z`KWwSl1=NCSA+@kt zL@lZoQ;Vx5)RJl`wX|ABEvuGO%c~XCimIzxNv*6_QLC!e)aq&td2f78)m`;aYpJ!> zI%-|Do?2gRpf*$+sg2bpYE!kD+FWg+wp2aUR;rikt@^0Gs-Nnw2B-!#P&KNp)gU!k z4N*hYFtv>uu12VlYLwbmZKt+ZJE$GiPHJbhi`rG~rgm3*s6EwQYHziV+E?wT_E)3T z0qQ{2qz+OCt3%YG>M(VSA??x>Q}JE>~BmE7eu%YITjeR$ZsAS2w5|)lKSVb&I-H z-KK6=cc?qnUFvRikGfagr|wq|s0YS6VWdQ?589#>DOC)HExY4wbHRz0VlS1+g+ z)l2GS^@@5`y{2AQZ>TrbTk37~j(S(Ur`}f|s1Mag>SOhZ`c!?UK389;FV$D-YxRx# zR(+?wS3js9)laG%nG^EX0HFyRVJqxJ9APgUgrkTn;)(bofk-G42`7P$VyRdrmWvf)rC23ai#1}cSSQwt4Pv9%BsPmJVyoCDwu>ENr`RQSi#=km*eCXj z1LB}KBo2!s;;1+#j*AoGq&Ou`i!x-bMr&iVvD!Foyf#6bs7=x)Yg4qT+B9vtHba}K&C+ITbF{hIJZ-+VKwGFS(iUq= zw58fIZMn8WTdA$mR%>gtwc0vuy|zKysBO|VYg@Ff+BR*wwnN*g?b3E@d$hgUK5f5t zKs%@%(hh4!w4>TF?YMSAJE@)0PHShhv)VcBymmpms9n-7Yge?Z+BNOEc0;?V-O_Gb z?(wmFkE3PrULkqc((hLq{&MkNq8!%6d)xnK?7_316kxAk{ydEX8#Z4CLd zJ=1BW#RKuqigf=GX#Tq9?XOFZ4zq^hH1P#{e{7 zAR4hX24OIUU?_%R8w|$?jKnBxi|w#IcEFC<2|HsK?26s6JNCey*b94OAMA_$us=rQ z033)W9E5{$2oA+zI2=cy8AswM9F1deERMtRH~}Z(B%F*>a4Js2={N&t;w+qvb8s%s z!}+)X7vdsZj7xASF2m)x0$1WHT#ajREw01$xB)lfCftl$a4T-Z?YIMX;x62cdvGuA z!~J*w58@#_j7RV&9>e2!0#D*8JdJ1YES|&jcmXfsCA^GR@G4%z>v#ii;w`+5cknLW z!~6IEAL1i?j8E_>KEvnu0$<`Qe2s7LExyC|_yIrSC;W^t6~C%lUl6FF4cej|#zA{@ zKu3&=@i0Cnz=W6xoiH&b!K9cBlVb`@iK#F(roptB4%1@>%!rvVGiJf8m<_XI4s^zx zm{5Fg=Ve1cE$89v7s_!3{?YkY%m@g2U$5BL#3;b)Yy1eTzpKn-os7VR(&+M@$H zVqA=e@i74=#6;+Xi7^Q##blTqQ(#I=g{d(Orp0ua9y4G@%!HXS3ueV^m>qMVGv>rx zm>ct8UUb2Hm>&yZK`exYu?QB$VptqYU`Z^6rLhc_#d264D_})*#Y$Kit6){EhSjkK zx?xRpM-QxpwXqJ?#d=sD8(>3hgpIKYHpOPx99v*Z^u$)^h2H3czUYVk7=Q*0L?gDw zAPmM348<^PgW(u~kr;(-u^qO@4%iVpVQ1`uU9lT>#~#=ddtq*F*PS_c{U{~yh-LVJu#9r7N`(R(}hy5`c2jD<7;UFB0 zLvSb#!{Imr%{UTA;bUuCPRAKI6KCOUoP%?59?r)FxDXfN zVqAhtaTzYh6}S>t;c8riYjGW}#|^j-exUdJ1F6K~;dyn}b~9^S_X_z)lA zV|;>7@fkkH7x)ri;cI+@Z}AlXV-ie?$uK#lz?7H@Q)3!Ti|H^uX26V?2{U6B%!=7CJLW)V%!#=$H|D{- z=z{q$KNi4(SO^Pa5iE+uusD{$l2{5$V;L-q<*+4UKm9R2a!Kzpdt78px!pqp ziecCW!!ZIQF$&vaJ8X{~Q2(xDJ#@m(*af>{H|&l*uqXDy-q;8GVn6JU(KrAHq6r7# zU>t%&aTpHA5opGdI0{GO7#xe^a6C@Hi8u)-;}o2V({MV@z?nD;XX6~4i}P?kF2IGj z2p8iLT#CzZIj+E!xC&R}8eEI(a6N9ojkpOn;}+bC+i*MXz@4}YcjF%1i~Ddt9>9Zm z2oK{CJc`HgIG(_hcnVMB89a;U@H}3?i+Bky;}yJ$*YG;tz?*mrZ{r=ji}&z8KEQ|g z2p{7Ue2UNTIljP`_zGX+8+?oJ@I8LOkN62cW6Z)n)z(^W2-MI9ZP582_QcQ-)F$Jc?RG1pmU|LLv=`jOl#7vkOvtU-thWhs`>p}mZzFARfZQcm$8)F+7eZ@FbqX z(|88Y;yFBz7w{rp!pnFCui`bljyLco-oo2>2k+uNypIp?AwI&#_ynKgGklIO@Fl*& z*Z2nC;yZkgAMhi7!p~^=l5$xdsCJfOK@q5-4cej|#zA{@Ku3&=@i0Cnz=W6xoiH&b z!K9cBlVb`@iK#F(roptB4%1@>%!rvVGiJf8m<_XI4s^zxm)<8F`iSFouwXinU!Ma!v z>th3Kh>fr@Ho>OY44Y#MY>A%O3cb)9eb5*E&>sWPfPrYl))<7r7=ob~hHWq$BQO%9 zur0R3_SgYCVkhj3U9c;5!|vDvdtxu_jeW2$_QU=djRSBXns5*f#vwQqhv9G>fo2?u zqi{5i!Lc|F$KwQ?h?8(KPQj@-4X5J_oQbn=HqODhI1lIJ0$hlTa4{~yrML{2;|g4f zt8g{0!L_&!*W(7kM!LxV{&*KHWh?np(Ucsw)4X@)3yotB)Hr~Ozcn|O61AK^&@G(BYr}zw?;|qL= zukba#!MFGh-{S}Th@bE?TK*=BUjN6T{znaM&=&154qE;;mi(v#&mA!?#>4oS025*& zbi%}#1e0PiOpYlqC8omEm$c`z@!U_Q)` z1+X9%!opYti()Y>jwP@pmcr6l2FqeOERPkiBFaA-VF{J6GFHK=SPiRV4Rphr=#Cy( z3u|K?tc&%qJ~qIH*a#bA6KsmjusOECmgtGC&Gt}jX@ZUAsC8b z*apKf0wXaB+hRLxj~%chcEZls1-oK5?2bLKC-%bL*a!P!KkSduH~cz=gO77vmCK zipy|0uE3SJ3RmMAT#M^)J#N5_xCuAo7Tk*4a69h6owy5k;~w0L`*1%Vz=L=Q591L$ zipTIcp1_lM3Qyx1Jd5Y>JYK+ycnL4#6}*bq@H*bWn|KRv;~l(<_wYVGz=!wp5^R>vCXhBeV0 zJ+Kzm#yVIR>tTItfDN$`HpV8{6q{jlY=JG&6I-DddZQ2eq96KW02(k5jo2E4Fc?EH z6vMC$hGPUqVidN;cGw;}U`OnPov{mc#ctRgdtguOg}t#4_QihKAER*q4nz|U!ofHM zhvG0Cjw8^FBXJat#xXb+$KiOKfD>^NPR1!X6{q2JoPjfO7S6^wI2Y&Pd|ZGFaS<-Y zCAbuq;c{GoD{&RB#x=MW*Wr5HfE#fWZpJOR6}RDb+<`lB7w*PAxEJ@~emsB&@em%y zBX|^#;c+~HC-D@X#xr;p&*6EzfEV!+UdAhU6|doSyn#3I7T(4?co*;CeSClq@ew}8 zC-@Yf;d6X}FYy(=#y9vD-{E`wfFJP_en!jRVwLrf>OlRE8rq;O+F=~DM+bDoxEK%P zV**TwiO>lXV-ie?$uK#lz?7H@Q)3!Ti|H^uX26V?2{U6B%!=7CJLW)V%!#=$H|D{- z=z{q$KNi4(SO^Pa5iE+uusD{$l2{5$V;L-q<*+4UKm9R2a!Kzpdt78px!pqp ziecCW!!ZIQF$&vaJ8X{~up@TD&e#RJVmIuLJ+LSC!rs^i`(i)rkI^^)2ciiF;b0ts zLva`m#}R18kvIxR;}{%^<8VAqz==2sC*u^Hiqmj9&cK;C3uogToQv~tJ}$t8xCj^H z5?qSQa5=8PmADF5;~HFx>u^18z>T;GH{%xEira8I?!cY63wPrl+>85gKOVq?cnA;U z5j={=@Hn2plXwbG;~6}Q=kPpUz>9bZFXI)wir4Tu-oTr93vc5cyo>knK0d&Q_y`~4 z6MTx#@HxJ~m-q@_;~RX7@9;f-z>oL|KcoCsU`tR@poTVRi*^_X?a=`pF)qf#_?Q3_ zVj^_H#Fzw=VlqsQDKI6b!qk`s(_%VIj~Or{X2Q&v1+!u{%#JzG8FOMT%#C?4FS=kp z%#Q`IAQr;HSOkk=F)WTHuq2kk(pUz|VmU026|f?@VkNAMRj?{n!|GTA-LNLQqX*W) z+E@qcVm+*n4X`0L!p7JHn_@F;jxDeydSWZ|LT~gzU-UzN3_t@0q7hqT5C&rihGH1D z!ElVgNQ}a^*bduc2keNQurqeSuGkH`V-M_!y|6d-!M@lJ`(rc?z=3GOK{yzP;7}Zf z!*K+faU_ny(KrUj;y4_S6L2CZzFARfZQcm$8) zF+7eZ@FbqX(|88Y;yFBz7w{rp!pnFCui`bljyLco-oo2>2k+uNypIp?AwI&#_ynKg zGklIO@Fl*&*Z2nC;yZkgAMhi7!p|uGCV;j6M}ZpJpe@>A9JEIVbi}wA594D3Oo)ll z2@_)yOp3`cIi|prmGt}jX@ZUAsC8b*apKf z0wXaB+hRLxj~%chcEZls1-oK5?2bLKC-%bL*a!P!KkSduH~cz=gO77vmCKipy|0 zuE3SJ3RmMAT#M^)J#N5_xCuAo7Tk*4a69h6owy5k;~w0L`*1%Vz=L=Q591L$ipTIc zp1_lM3Qyx1Jd5Y>JYK+ycnL4#6}*bq@H*bWn|KRv;~l(<_wYVGz=!wqXoI$BhjGvz9ncZuVmyqG2{0ihLMKd& zNiZoU!{nF(Q(`JijcG6~ro;4@0W)GI%#2wuD`vy&m;;?LC+5Q3mD!}YiUH{vGTj9YLkZo}=k19##s+>Lv1FYd$rcmNOLAv}yn@F*U`<9Gs3 z;we0hXYeeZ!}E9nFXAP8n18?Fjyp4D8F5biY_y8Z`BYccc@F_mS=lB9& z;wyZOZ}2U?!}s_BKjJ6+jPmaRTI+ulsG$woq8-LTdvriYjEnIwJ|@6~m`>7=z+DcHrBzqSP$!C z18j(murW5lrq~RdV+(AFp4bY#&>MZw7yZy51JHnhXvEeSguxhsp%{j3FdQQ=5~Hv! zw!`+=0Xt$R?2KKoD|W-~*aLfFFYJwdurKz*{uqq|a3Gp+5Dvy6I24EBa2$bV9EqcF zG>*ZsI1b0-1e}PIa57H8sW=U%;|!dMvv4-f!MQjO=i>rgh>LJBF2SX^442~yT#2i2 zHLk(6xDMCj2Hc37a5HYft+)-h;||=3yKpz|!M(T-_u~OPh==en9>Jq{43FapJc+09 zG@ik;cn;6w1-yut@G@S(t9T8s;|;utx9~RJ!Mk`5@8bh}h>!3wKEbE>44>l*e2K5{ zHNL^O_zvIW2mFYi@H5K425hbWQJ{u4Xp43j2kp@T9WgG(!}yp06JjEC!o-*alVUPV zjwvuDroz;i2Ge3XOph5bBWA+Pm<6+9Hq4GW&>3@LF3gR2FfY1bKFp5=upkz~!dL{0 zVlga^C9oux!qQj<%VIe!j}@>Yx?&})j8(8IR>SI81KqGDx}yiy!rE8|>ta2uj}5RP zHp0f(1e;q9kC3<2j^g?g+L0|Mke+)na2BHyLV-N;o2!>)9w!v_Wz(|b3w%88a zV+ZVrov<@@!LHa1yJHXRiM_Bl_QAf`5Bp;@4#0tE!a+C~hu}~ghQo0LnsFqK!qGSe z$Kp5~j}verPQuAJ1*hUPoQ^YaCeFgyI0xtAJe-dUa3LSeNC+@=CxCi&*KHQH7@E{(-!*~Rb;xRmqC-5Ym!qa#L z&*C{ej~DPFUc$?G1+U^YypA{UCf>r^cn9y|J-m+(@F70J$M^)F;xl}XFYqP4!q@l) z-{L!bk006e%6JrugipelJrofb# z3R7bmOpEC-J!Zg+m;O(V-YNh#jrS* zz>-)BOJf-7)R4Xa}fbiLgWIkv!-=!vb+3%$_?ebEp7F#ru1h(>IUK^Tl77>Z%o2E#D|BQXlwVmoY)9k3&I z!p_(QyJ9!&jyw+=|<9JMO@pxC?jV9^8xja6cZvgLnuJ;}JZH$M86wz>|0iPvaRpi|6n> zUcifZ2`}Rnyo%TGI^MvWcnfdi9lVS8@IF4khxiB|;}d*}&+s|Ez?b+6U*j8mi|_C~ ze!!3T2|uHfi25G|YG{MDXoqpo9v#pT<6=CFj|ng#CPF7nj7cylCd1^I0#jltOpR$U zEvCctm;p0lCd`akFe_%m?3e?cF(>B2+?WURq6_B3{8#`BVj(PyMX)Fq!{S&1OJXT3 zjb*Sbmc#N`0V|>_R>I0y1*>8;td2F%4Qrx1dSETAjdidt*2DVP02^W>Y>Z8?DK^9A z*aBOkC$>T_^hO`_ML+b%05o7A8nHD7VK9bZD28Dh495tJ#3*cw?XW#|z>e4nJ7X8@ zirug~_Q0Ol3wvW9?2G-dKStvK9Ec_ygoAMi4#iGXd0Vm=l zoQzX&Do(@cI0I+mES!yVa4ycn`M3ZV;v!s(OK>SJ!{xXFSK=yMjcaf%uEX`X0XO0% z+>BdrD{jN>xC3|MF5HcKa4+t|{dfQm;vqbYNAM^f!{c}YPvR*&jc4#Ip2PEa0Wabu zyo^`yDqh3ucmr?ZExe6)@GjoN`}hDK;v;;FPw*)|!{_({U*ao#jc@QRzQgzU0YBm= z{EUhd^*;*K&<1VM4&$IbI-n!Q#dsJW6JSD2gie?klVDOzhRHDnro>d38q;7}Oo!<) z17^fbm>IKRR?LRkF$X$hPRxb5F%RZN7tDwGu>cmtLRc7!U{NfF#jymI#8Oxq%V1e7 zhvl&XRzz2f*q9c!Q))tJ21hxM@mHpE8Q7@J^IY=+IT1-3*_ zY=vIvjXvm$e&~+@Xuv=;VrvY-U<|=f48t}Uju9A%QP>vSVSDU=9kCO3#xB?uyJ2_i zfjzMo_QpQg7yDst5KTA;2jdVNioY> zoQBhJ2F}D;I2-5ST%3pVaRDyGMYtH3;8I+M%W(y+#8tQ&*Wg-QhwE_zZp2Nv8Mok8 z+=kn62kyjOxEuH2UfhTK@cNB9_@;8T2t&+!Gm#8>zl-{4z(hwt$Ne#B4s8I{D; z|0qyH8?;3`jDz;*fQ}d!<6(SEfC(`XI$>f=f=MwMCdU+*5>sJnOoM4L9j3<&m=QB! zX3TJeU_mq=6{}%&tbuM=6W!4RYhi7ygLSbU*2f0e5F24*Y=TX(88*ij*b+Ul6?&mJ z`k*iRp+5$o0Rz#9tuY9LF$6;~4BKEhMqngHVOwm6?Xd%P#7@{5yI@!BhTX9T_QYP; z8~b2i?1%j^8VBG&G~pl|j6-lJ4#VL%0?jxQN8xB3gJW?Vj>ic&5hvkfoPtwv8cxR< zI1^{#Y@CC0aURac1-K9w;bL5ZOK}-4#}&8|SK(@0gKKdeuE!0y5jWvx+=5$i8*axP zxD$8bZrp==aUbr-19%V*;bA<2NAVaQ#}jxGPvL1igJx4=M$CknF$-qJ zY?vK$pfl#gT$mg4U|w{=e3%~#U_mT|g|P@0#bQ_-OJGSXg{83!mc?>d9xGr)bj3|SQBeuZLEWJu^!gP2G|fAVPkB9O|cm^#}?QU zTVZQ#gKe=Lw#N?G5j$aL?1Ejf8+OMY*b{qUZ|sA8u^;xw0XPr`;b0tsLva`m#}POZ zN8xB3gJW?Vj>ic&5hvkfoPtwv8cxRfCfVt z2E$@F437~oB1Xc<7zLwZG>nchFeb*r*cb=nVmyqG2{0ih!o-*alVUPVjwvuDroz;i z2Ge3XOph5bBWA+Pm<6+9Hq4GWFem21+?WURVm{1|1+X9%!opYti()Y>jwP@pmcr6l z2FqeOERPkiB38o6SOu$MHLQ*`uqM{R+E@qcVm+*n4X`0L!p7JHn_@F;jxDeyw!+rf z2HRpgY>yqVBX+{h*af>{H|&l*uqXDy-q;8GVn6JU18^V?!ofHMhvG0Cjw5g+j>6G6 z2FKz!9FG%lB2L1|I0dKTG@Onza3;>e**FL1;yj#>3veMW!o|1*m*O&9jw^5_uEN#0 z2G`;`T#p-YBW}XYxCOW3Hr$Roa3}7<-M9z$;y&Du2k;;s!ozq3kK!>rjwkRWp2E|3 z2G8O-JdYRfB3{DFcm=QGHN1{D@Fw2E+js}>;yt{N5AY#A!pHaopW-uojxX>fzQWh| z2H)a4e2*XSBYwiq_yxb>H~fx2@F)Jl-}ndr;y<*a(0_E$MGpl^RH)I%01bvP42H#U z7#<^FM2v)yF$zY-Xc!%1U`&jKu`v$D#dsJW6JSD2go!Z;CdFi!98+LQOogd24W`9( zm>x4=M$CknF$-qJY?vK$U{1`1xiJss#eA3_3t&MkgoUvP7R6#%97|wHEQO`943@=m zSRN~2MXZFCu?kkjYFHg>U`?!rwXqJ?#d=sD8(>3hgpIKYHpOPx99v*ZY=y0{4YtL0 z*d9AzN9=^1u?u#^ZrB}rU{CCYy|EAW#eUcy2jD;)goAMi4#irsL98cg$JcXz644%bv zcpfj{MZAQU@d{qWYj_=R;7z=RxA6|%#d~-kAK*iLgpctFKE-GF9ADr|e1)&^4Zg*9 z_#QvtNBo4J@e6*%Z}=U5;7|O8zwrv(J(s3z?c{dV`ChQi}5f%CcuQ42oqxxOp3`cIi|prm85)v!9&z?xVKYhxX(i}kQRHo%712peM)Y>LgWIkv!-*a}-?8*Gd1uswFb zj@Su1V;Ag--LO0Mz@FF(dt)E$i~X=a4#0sp2nXX39E!tmIF7)PI0{GO7#xe^a6C@H zi8u)-;}o2V({MV@z?nD;XX6~4i}P?kF2IGj2p8iLT#CzZIj+E!xC&R}8eEI(a6N9o zjkpOn;}+bC+i*MXz@4}YcjF%1i~Ddt9>9Zm2oK{CJc`HgIG(_hcnVMB89a;U@H}3? zi+Bky;}yJ$*YG;tz?*mrZ{r=ji}&z8KEQ|g2p{7Ue2UNTIljP`_zGX+8+?oJ@I8LO zkN62c;}`sj-|##Bz@PXFf8!tgi~rDyM*q=47d;dxQK3d312h=IFc=oYVR(#y5it@* z#wZvSqhWN6fiW=_#>O}p7vo`kOn?b75hlhYm=u#?a!i3KF%_o9G?*6CVS3Df88H)P z#w?f>vtf43fjKc3=Egjj7xQ6$EPw^E5EjNFSQLw4aV&u)u@siZGFTSNVR@{86|oXl z#wu79t6_DlfiY6LAtw z#wj=zr{Q#*firOy&c-=77w6%8T!0I45iZ6hxD=P+a$JEcaTTt{HMkbn;d@fE(tH~1Fc;d}gmAMq1@ z#xM94zu|ZMfj{vV{>DG}7yqFZo&KYPE_x_XqC$;6252yZVK6L)!|)gZBVr_sj8QNu zM#JbB17l(=jE!+HF2=+7m;e)EB20`)FexU(SI818ZU}tc`WBF4n{P*Z>=1BW#RKuqigf=GX#TVk>NoZLlr2!}iz#J7Op7j9suR zcEj%21AAgG?2Ub}FZRR!H~D!}YiUH{vGTj9YLk zZo}=k19##s+>Lv1FYd$rcmNOLAv}yn@F*U`<9Gs3;we0hXYeeZ!}E9nFXAP8n18?Fjyp4D8F5biY_y8Z`BYccc@F_mS=lB9&;wyZOZ}2U?!}s_BKjJ6+j9>68 ze#7th1ApQ#{EdI`FaAR-2K`3|UGz|(M1>lC4A5W*!(dnphv6{-M#M-M8KYoSjE2!M z2FAo#7#rhYT#SeDF##sTM3@+pU{Xwm$uR|{#8j9X(_mUmhv_i`X2eXG8M9zk%!b)9 z2j;|Fm>ct8Ud)I2u>cmtLRc7!U{NfF#jymI#8Oxq%V1e7hvl&XR>VqJ8LMDbtcKOG z2G+z{SR3nLU95-ou>m&3M%WmeU{h>{&9Mcx#8%iE+hAL4hwZTgcEnED8M|Ot?1tU3 z2lm8X*cY>oQBhJ z2F}D;I2-5ST%3pVaRDyGMYtH3;8I+M%W(y+#8tQ&*Wg-QhwE_zZp2Nv8Mok8+=kn6 z2kyjOxEuH2UfhTK@cNB9_@;8T2t&+!Gm#8>zl-{4z(hwt$Ne#B4s8Nc9H{D$B0 z2mZug_#6M=U;KwwO!|)wy6B-mi3&CP7@)xrhQY8H4#Q&vjEIpiGDg9u7!9Li42+4f zFgC`)xEK%PV**Twi7+uH!K9cBlVb`@iK#F(roptB4%1@>%!rvVGiJf8m<_XI4$O(U zFgNDGyqFL3V*xCPg|ILd!J=3Ui(?5aiKVbKmcg=E4$ET&tcaDcGFHK=SPiRV4XlZ^ zur}7gx>yhEV*_l6jj%B`!KT;@n_~-XiLJ0Tw!ya84%=e~?1-JPGj_qQ*bTd55A2D( zus8O>zSs}@;{Y6pgK#ho!J#+|hvNtwiKB2dj=`}w4#(pJoQRWfGETv%I1Q)c44jFx za5m1txi}B!;{sfWi*PY6!KJtim*WatiK}omuEDjq4%g!b+=!cSGj74HxDB`C4%~^m za5wJ3y|@qe;{iN~hwv~S!J~K#kK+kEiKp;1p24$t4$tESyoi_ZGG4)}cnz=P4ZMlB z@HXDTyLb=p;{$w%kMJ=*!Ke5PpW_RBiLdZAzQMQn4&UPk{D`0MGk(FZ_zl0~5B!P0 z@HhU!zxWTWSo9wqbkReB5*2FnF+hVM41-}Y9EQgT7!f03WQ>AQF&ak47#I^{VQh?p zaWNjo#{`%V6JcUZf=MwMCdU+*5>sJnOoM4L9j3<&m=QB!X3T_y7RM4;5=&ueEQ4jS9G1rlSP?5>Wvqf#u^Lv#8dwu+VQs8~ zb+I1S#|GFC8)0K?f=#g*Hpdp&5?f(wY=dpF9k#~~*bzHnXY7Jqu^V>B9@rCmVQ=h% zeX$?*#{oDH2jO5GfxDhwuX54~XaT{*O9k>&B;cnc6 zdvPD`#{+l}58+`vf=BTf9>)`S5>Mf2JcDQP9G=Guco8q*WxRq{@fu#o8+a3M;cdKw zckv$H#|QWjAK_zsf=}@oKF1gM5?|qKe1mWC9lpm8_z^$hXZ(U+@f&`}ANUi0;cxtd zfAJq$vFSfL=%R-LB`VbDV}J%j7zV>)I1G;wFd|06$QT8qVl<47F)${^!q^xG<6=CF zj|ng#Cc?y+1e0PiOpYlqC8omEmta2u zj}5RPHp0f(1e;q9kCAPxW*aq8TJ8X{~up@TD&e#RJVmIuLJ+LSC!rs^i`(i)r zj{|TZ4#L4W1c%}<9F8M!B#y$-I0nb!I2?}?a3W5^$v6e4;xwF&GjJx(!r3?n=i)q^ zj|*@iF2cpQ1efA6T#hSnC9cBNxCYnaI$Vz%a3gNQ&A0`(;x^olJ8&oN!rizB_u@X> zj|cD|9>T+T1drk|JdP*uB%Z?4cm~hnIXsUS@FHHq%XkH^;x)XEH}EFj!rOQU@8UhY zj}P!6KElWN1fSwFe2y>hCBDMf_y*tNJA98H@FRZ0&-ewu;y3(`Kkz61!r%A@|KdNi z;?RF|&_xdgN>r%P#{dn6Fbsypa2OsVU_^|BkueHJ#b_8EV_-~-g|RUX#>IFT9}{3g zOoWLs2`0s4m>g4JN=${RF%720beJA9U`EV@nK27y#cY@zb6`%)g}E^g=EZ!N9}8eX zEQE!z2o}X+SR6}WNi2n>u?&{Qa#$WKU`4Eim9Yv|#cEg`YhX>Rg|)E`*2Q{Q9~)pp zY=n)m2{y%M*c@A6OKgR$u?@DxcGw;}U`OnPov{mc#ctRgdtguOg}t#4_QihK9|zz- z9E5{$2oA+zI2=ddNF0TuaSV>daX20);6$8+lW_`8#c4PlXW&eng|l%E&c%5+9~a<4 zT!f2p2`Lkg}ZSN?!|q$9}nO` zJcNhw2p+{_cpOjQNj!z8@eH2Db9f#v;6=QIm+=Z-#cOySZ{SV5g}3nz-o<-(A0OaD ze1wnj2|mSV_#9v0OMHc|@eRJkclaJZ;79y~pYaQR#c%i>f8bC2g}?C+{>6W2#ijq~ zpo<;~l&Danj{zDCVHgaH;V?W#z=#+LBV!bdiqSAS#=w{u3u9v(jEnIwJ|@6~mJs)Gh-IairFwb=D?ho3v**0%!~OjKNi4(SO^Pa z5iE+uusD{$l2{5$V;L-q<*+*1(!r3u|K?tc&%qJ~qIH*a#bA z6KsmjusOECme>kgV;gLX?XW#|z>e4nJ7X8@irug~_Q0Ol3wvW9?2G-dKMufwI0y&h z5FCoba5#>@kvIxR;}{%^<8VAqz==2sC*u^Hiqmj9&cK;C3uogToQv~tJ}$t8xCj^H z5?qSQa5=8PmADF5;~HFx>u^18z>T;GH{%xEira8I?!cY63wPrl+>85gKOVq?cnA;U z5j={=@Hn2plXwbG;~6}Q=kPpUz>9bZFXI)wir4Tu-oTr93vc5cyo>knK0d&Q_y`~4 z6MTx#@HxJ~m-q@_;~RX7@9;f-z>oL|KjRntir?@%{=lF33xDGu{EPq4ibwy^K^Hv~ zC{dwC9|JTP!Y~*X!(n)gfDthgM#d-@6{BHvjDayR7RJUn7#HJVd`y4|F%c%lB$yPF zVRB4?DKQnM#x$4~(_wndfEh6pX2vX-6|-S>%z-&E7v{!1m>2V5ek_0mu@Dxmq=6{}%&tbsML7S_f(SQqPIeQbaYu@N@LCfF34 zVRLMOEwL50#x~d%+hKd`fE}?DcE&E)6}w?~?14S87xu^NPR1!X6{q2JoPjfO7S6^wI2Y&Pd|ZGFaS<-YCAbuq z;c{GoD{&RB#x=MW*Wr5HfE#fWZpJOR6}RDb+<`lB7w*PAxEJ@~emsB&@em%yBX|^# z;c+~HC-D@X#xr;p&*6EzfEV!+UdAhU6|doSyn#3I7T(4?co*;CeSClq@ew}8C-@Yf z;d6X}FYy(=#y9vD-{E`wfFJP_e#S5O6~Ezk{DD957yiaS_!s}76`%g2gD!d~P@+PO zJ_cwogkdl&hQsg}0V850jEqq*Dn`TT7z1NsER2nDFfPW!_?Q3_Vj@h8NiZoU!{nF( zQ(`JijcG6~ro;4@0W)GI%#2wuD`vy&m;-ZSF3gR2FfZoA{8#`BVj(PyMX)Fq!{S&1 zOJXT3jb*Sbmc#N`0V`r9tc+E#DptelSOaTfEv$`ourAia`q%&)Vk2yfO|U68!{*om zTVgA0jcu?kw!`+=0Xt$R?2KKoD|W-~*aLfFFYJwdurKz*{x|>!;vgK1LvSb#!{Imr zN8%_Pjbm^uj>GXd0Vm=loQzX&Do(@cI0I+mES!yVa4ycn`M3ZV;v!s(OK>SJ!{xXF zSK=yMjcaf%uEX`X0XO0%+>BdrD{jN>xC3|MF5HcKa4+t|{dfQm;vqbYNAM^f!{c}Y zPvR*&jc4#Ip2PEa0Wabuyo^`yDqh3ucmr?ZExe6)@GjoN`}hDK;v;;FPw*)|!{_({ zU*ao#jc@QRzQgzU0YBm={ET1lD}KZ8_yd39FZ_*v@Gt&DD}nWYpN$T>=%GM~3N`u| zpurG^!LS$(!(#-Dh>7)R4Xa}ftckU-HrBzqSP$!C18j(murW5lrq~RdV+(AF zt*|w=!M4~A+hYgph@G%AcEPUL4ZC9x?1{awH}=84*bn>T033*ea4-(Rp*ReO;|Lsy zqi{5i!Lc|F$KwQ?h?8(KPQj@-4X5J_oQbn=HqODhI1lIJ0$hlTa4{~yrML{2;|g4f zt8g{0!L_&!*W(7kM!LxV{&*KHWh?np(Ucsw)4X@)3yotB)Hr~Ozcn|O61AK^&@G(BYr}zw?;|qL= zukba#!MFGh-{S}Th@bE?e!;K!4Zq_L{E5HtH~zuD_z$gw^dB8`(L;d}6>9V`K!YI+ zgJCfohQ|mP5hGz_jDk@y8b-$$7!zY*Y>b0(F&@Up1eg#LVPZ^zNii8F#}t?nQ(|SQBeuZLEWJu^!gP2G|fAVPkB9O|cm^#}?QUTVZQ# zgKe=Lw#N?G5j$aL?1Ejf8+OMY*b{qUZ|sA8u^;xw0XPr`;b0tsLva`m#}POZN8xB3 zgJW?Vj>ic&5hvkfoPtwv8cxRqK5({D%9v>fCfVt2E$@F z437~oB1Xc<7zLwZG>nchFeb*r*cb=nVmyqG2{0ih!o-*alVUPVjwvuDroz;i2Ge3X zOph5bBWA+Pm<6+9Hq4GWFem21+?WURVm{1|1+X9%!opYti()Y>jwP@pmcr6l2FqeO zERPkiB38o6SOu$MHLQ*`uqM{R+E@qcVm+*n4X`0L!p7JHn_@F;jxDeyw!+rf2HRpg zY>yqVBX+{h*af>{H|&l*uqXDy-q;8GVn6JU18^V?!ofHMhvG0Cjw5g+j>6G62FKz! z9FG%lB2L1|I0dKTG@Onza3;>e**FL1;yj#>3veMW!o|1*m*O&9jw^5_uEN#02G`;` zT#p-YBW}XYxCOW3Hr$Roa3}7<-M9z$;y&Du2k;;s!ozq3kK!>rjwkRWp2E|32G8O- zJdYRfB3{DFcm=QGHN1{D@Fw2E+js}>;yt{N5AY#A!pHaopW-uojxX>fzQWh|2H)a4 ze2*XSBYwiq_yxb>H~fx2@F)Jl-}ndr;y<(!(|>f(MGpl^RH)I%01bvP42H#U7#<^F zM2v)yF$zY-Xc!%1U`&jKu`v$D#dsJW6JSD2go!Z;CdFi!98+LQOogd24W`9(m>x4= zM$CknF$-qJY?vK$U{1`1xiJss#eA3_3t&MkgoUvP7R6#%97|wHEQO`943@=mSRN~2 zMXZFCu?kkjYFHg>U`?!rwXqJ?#d=sD8(>3hgpIKYHpOPx99v*ZY=y0{4YtL0*d9Az zN9=^1u?u#^ZrB}rU{CCYy|EAW#eUcy2jD;)goAMi4#irsL98cg$JcXz644%bvcpfj{ zMZAQU@d{qWYj_=R;7z=RxA6|%#d~-kAK*iLgpctFKE-GF9ADr|e1)&^4Zg*9_#Qvt zNBo4J@e6*%Z}=U5;7|O8zwrv(J(s3z?c{dV`ChQi}5f%CcuQ42oqxxOp3`cIi|prm85)v!9&z?xVKYhxX(i}kQRHo%712peM)Y>LgWIkv!-*a}-?8*Gd1uswFbj@Su1 zV;Ag--LO0Mz@FF(dt)E$i~X=a4#0sp2nXX39E!tmIF7)PI0{GO7#xe^a6C@Hi8u)- z;}o2V({MV@z?nD;XX6~4i}P?kF2IGj2p8iLT#CzZIj+E!xC&R}8eEI(a6N9ojkpOn z;}+bC+i*MXz@4}YcjF%1i~Ddt9>9Zm2oK{CJc`HgIG(_hcnVMB89a;U@H}3?i+Bky z;}yJ$*YG;tz?*mrZ{r=ji}&z8KEQ|g2p{7Ue2UNTIljP`_zGX+8+?oJ@I8LOkN62c z;}`sj-|##Bz@PXFf8!tgi~rC{O8?P87d;dxQK3d312h=IFc=oYVR(#y5it@*#wZvS zqhWN6fiW=_#>O}p7vo`kOn?b75hlhYm=u#?a!i3KF%_o9G?*6CVS3Df88H)P#w?f> zvtf43fjKc3=Egjj7xQ6$EPw^E5EjNFSQLw4aV&u)u@siZGFTSNVR@{86|oXl#wu79 zt6_DlfiY6LAtw#wj=z zr{Q#*firOy&c-=77w6%8T!0I45iZ6hxD=P+a$JEcaTTt{HMkbn;d@fE(tH~1Fc;d}gmAMq1@#xM94 zzu|ZMfj{vV{>DG}7yqG^jQ*p8E_x_XqC$;6252yZVK6L)!zoiM|NjCecb?p^dDOC9 zhEA?Hx#d*bvPN`i*gRhIgr%$@lba_V_TS&+rR*}{lMR{NvZ?%Uv|#+Q|2xsP9Lu#l zOIXrUmbQE=u#6S5!dPLga8`ILf)&wRthVnmC8zOrLodl>8$it1}meL$;xbHv9em(tn5|}E2ovq%5CMb z@>=<<{8j<0pjF5!Y!$JJTE(p5Rtc-5Rmv)Dm9ffN<*f2n1*@V}$*OEsv8r0ttm;+` ztEN@Us%_P=>RR=z`c?z0q1DK0Y&Ef(TFtEHRtu}8)yisZwXxb-?X31z2dksi$?9x% zvASB_tnOA1tEbh=>TUJ0`da<0{?-6%pf$)EYz?u7TEnd2)(C5)HOd-ojj_gB#X(G25Y0W$=Ymfv9?;$zow3eZ=dAPA1?!@9$+~P^v94Oztn1bd>!x+fx^3OD?ppV(`_=>Nq4mgm zY(24_TF!Jc1Amso!QP}XSK80+3g&5 zPCJ*K+swe#8e?E-c|yO3SjE@Bt8i`m8P5_UW; zSGB9z)$JN~O}my|+pc5Rwd>jS?FM#3yOG`4ZelmJo7v6n7IsU!mEGEIW4E>2+3oEP zc1OFD-P!J9ceT6O-R&NBPrH}h+wNocwfouq?E&^cdyqZY9%2u*huOpJ5%x%Xls(!W zV~@4R+2idA_C$M+3W2M_C|Y?z1iMkZ?(7C+wC3pPJ5TV+umdEwfEWk?F05f`;dLu zK4KrWkJ-oV6ZT2_lzrMhW1qFp+2`#G_C@=Wec8TZU$w8<*XT*q^SBOT>v$9Dq9I3XvD6V?gmgm)r15uHd*WG9Lf)rsarcVaj( zomft6Cyo=>iRZ+35;zH+L{4HSiIdbx<|KDgI4PY}PHHEOlh#S+q<1nn8J$c{W+#i2 z)yd{$cXBv6om@_CCy$fY$>-#E3OEIwLQY|)h*Q)l<`j2II3=A@PHCr%Q`RZxly@pP z6`e{>Wv7Z$)v4xGcWO8_omx(9r;bzCspr&p8aNG|Mowd=iPO|+<}`O&I4zx4PHU%) z)7EL{w0Al<9i2{2XQzwP)#>JRcX~KIonB6Fr;pRu>F4x!1~>zqLC#=jh%?j~<_vd6 zI3t}=&S+)w$+ecWyX0om9ykx3N6usCiSyKX<~(;^I4_-7&THq5^VWIiymvl0ADvImXXlIa)%oUp zcYZiOonOvx=a2K(`S;(`Z@Z4`x}Gas=_*&dz8kp44Y^_5ux>avyc@xd=tgoQyHVVz zZZtQ#8^ew1#&To3aoo6WJU70Zz)k2Taud5r+@x+YH@Ta_P3fj`Q@d&0v~D^#y_>ecZloKexX-z#ZrgatFIZ z+@bC;cep#k9qEp8N4sO(vFTx+@_qcn)J?WluPrGN_v+gx*z80{gduhD1 zUOF$mm%+>EW%4q6S-h-XHZQxE!^`RA@^X85yu4mMFTYp7E9e#S3VTJoqFyntxL3j} z>6P+Idu6<`UOBJ4SHY|3Rq`r(RlKTRHLto?!>j4l@@ji^yt-aJufEs7Yv?ud8hcH= zrd~6zx!1yL>9z7&du_b7UOTV7*TL)Pb@DoUUA(SdH?O5cM6dtyZ@#y{Tj(wF7JEy)rQR}cxwpbw>87DXUduP0}-Z}5Qcfq^pUGgq_ zSG=pAmt^dvCnA-aGHT z_rd$;fp{R5fWiUSP@Qy z7ZF565lKWAQAAV`O+*(lL`)G?TxToF&i7YRf{kw_#KNkmeSOe7a6L`soLq!wvJ zT9Hnq7a2rGkx670SwvQmO=K52L{5=Q`GML{HI6^cH7rC#7?nG>=t{(Ua?Q?7YD>a zaY!5%N5oNaOdJ;{#7S{VoEB%qS#eIB7Z=1uaYr>!^!Y6f{Z95$;dK_j4Gqa=rV?kDPzglGLDQZ zi~%qp|V>@tVUDRar( zGLOtF^U3_OfGj8r$-=UTEGmo1;avEc zDQn5vvW~1P>&g1Efov!n$;PsYY$}_{=CXxsDO<_bvW;vj+sXE_gX}0f$?*s- z?y`sMDSOG@vXAU5`^o-tfE*|X$-#1n94d#&;c|o=DM!iCa*P}+$I0<>f}AKP$;onx zoGPcu>2ijgDQC&qa*muU=gIkUfm|pT$;EPsTq>8z<#L5wDObtWa*bRo*U9yAgWM=L z$<1<$+$y)p?Q)0QDR;@;a*y0A_sRY8fIKJ<$;0x9JSvaL+*)YDR0T!@{YVK@5%e}fqW<*$;a}Ed@7&G=kkSoDPPIg@{N2e z-^us#gZwBz$P%2BTJlu%MBrIoJ&WmHIoQDId$ z6<$S95mh7=Sw&G%RWub{#ZWO-EEQYDQE^o~6<;M#2~{GMSS3+ORWg-arBEqVDwSHL zQE62=m0o2~8C52gS!GdKRW_Af4RbMqw4OJu6ST#{iRWsFG zwNNcpE7e-HQEgQ_)n0W_9aSgQS#?oeRX5dL^-w)kFV$Q1QGHcE)n5%z1JxikSPfA_ z)i5<&jZh=iC^cG*QDfCOHC|0n6V)U&Sxr$>)igC-%}_JdEHzurQFGNiHD4`I3)LdE zSS?XY)iSkQtxzk~Dz#dzQESyYwO(yd8`UPYS#42U)i$+V?NB?_F11_jQG3-swO<`j z2h|~USRGME)iHHkolqy$DRo+%QD@aTbzWUi7u6+oSzS?A)irfp-B35xEp=PnQFqln zbzePD57i^}SUpis)id>6y-+XJEA?8vQE$~d^j*lcj-(^&C_1W+rlac^I;M`LW9v9N zu8ybU>jXNXPNWm-Bs!^1rjzRwI;BpfQ|mN3txl)Y>kK-h&ZINzEIO;srnBoDI;YO1 zbL%`hug<6Q>jJuk7J}uB0pLD!Qt!rmO23 zx~8tBYwJ3?uCAx+>jt`^ZloLQCc3F^rkm>)x}|QVTkAHut!}5=>khi3?xZ{GF1oAk zrn~DNx~J}?d+R>BukNS&>j8S89;65BA$q7DribehdZZquN9!?qtRAPw>j`?Io}?%1 zDSE1&rl;!}dZwPGXX`n7uAZmo>jiqDUZfZ6C3>k|rkCp#dZk{aSL-!;tzM_s>kWFN z-lR9{Eqbfornl=IdZ*r{ck4ZRuimHk>jV0rKBN!pBl@U5rjP3r`lLRkPwO-KtUjmD z>kIm#zN9bfEBdOwrmyQ8`li06Z|ghyuD+-5>j(Owexx7kC;F*=rl0E<`lWuQU+Xve zt$wH9>ks;){-i(aFZ!$groZbS`ltS-f9pT`ul}bk-}W8f^*vws(pSFreLwJxAM(Ta zVf}D^ct3(4(U0Uu_M`Yw{b+u4KZYOEkLAbq(_Otj|{cL`AKZl>w&*kU#^Z0rFe13kvfM3ur-cs3dVYPs zf#1+?^j zdH;fc(ZA$h_OJL?{cHYp|Av3lzvbWd@A!B9d;Wd@f&b8df5(bHa#6glE zX^<>P9;66T2C0J7L7E_KkS<6cWC$__nS#tgmLO}8Eyy0^2yzCwg4{u#Aa9T_$R894 z3I>IO!a(<%76yxg#lezbX|OC<9;^sf2CIVA!J1%gur631 zYzQ_6n}W^3mSAhJE!ZCH2zCa$g5ANMU~jN5*dH7S4hDyU!@-f@XmBhz9-IhH2B(74 z!I|J}a4t9>TnH`(mx9Z|mEdY{Ew~=s2yOpu}vHk*TggNO#+k9Br=Ij z5|h*}%>Xmd3^Iew5Hr*aGsDdYGt!JQqs*;iOfr+r6f@OKGtuK~EHaDD60_7SGt12iv(l_GtIZm-)~qw@%?7j4Y%-h87PHlCGuzD$ zv(xM{yUiZ6*X%R<%>i@J95RQ^5p&cWGsn#dbJCnLr_C92)|@lv%>{GOTr!u<6?4^G zGuO=xbJN^1x6K`M*W5Gr%>(n$JTi~X6Z6zOGtbQn^U}OBugx3t*1R+C%?I<*d@`TS z7xUG8GvCb*^V9q?zs(=>*Zeb9$PPInH{^vxNQP8Mhx||wGNDi?Oekz9Tqt}fLMUP= zQYdmLN+@b5S}1xbMkr<|Rw#BTPAG0DUMPMjK`3D;Q7CaJNhoP3StxlZMJQz`RVZ~R zO(<>Xf7*J>z@V9>ZMZ-wmfg)pHttf~-Q5bsT}pvMTcCvs?k>fPySux)ySux)+gsq= zN1pHdk<9GOm1`us`8ml&<)-pbd8vFI8L? zIz^qP&QNEmbJTh20(FtPL|vw?P*ILcd6U+j$!5lCb z%meem0CuoNr<%fSk;608EN!5Xj@tOM)82Cxxq0-M1WuoY|r+rbX76YK)J z!5**|>;wD30dNo;0*Ap7a1y*SOHdqm0)F91y+S2uo|omLtzbA6V`&YVI5c()`Rt71K1EYf{kGl z*c3K{&0!d90mETS*b26W5wHzx3nO6^jE3!Cd)NWSz*yK3c7ky*9wxv<*co<#U12xa z9rl1dVK3Ml_JMt2KiD4*fCJ$mI2aCrL*Xzu9FBlVa3mZBN5e62EF1^N!(=!CPK1-- zWH<#*h11}4I0Mdvv*2tv2hN4_;C#3ME`*EVVz>k@h0EY_xB{+(tKe$52Cjwc;Ci?L zZiJiQX1E1zh1=kExC8ElyWnoP2kwRY;C^@j9)ySBVR!@{g~#A=cmke;r{HOL2A+lI z;CXlfUWAw6Wq1W%h1cM9cmv*qx8QAf2i}GE;C=W2K7^0pWB3F)D3kL5JYtcHi9&JDy(I&JRZ9!YnHnbh>Ks(Vcv>WX~d(l3$A00pk(IIpg z9YIIYF?1ZAKqt{DbQ+yOXVE!y9$i2e(Is>lT|rmTHFO=_KsV7XbQ|44chNm`A3Z=1 z(IfO2JwZ>=GxQw2KrhiN^cuZEZ_zvS9(_O`(I@m7eL-K*H}oC-KtIth6hNnC=CT-C+?a(gm z(LNnaXQs2zS?O$ab~*>0lg>rwrt{Ew>3np4x&U2}E<_imi_k^sVsvr31YMFYMVF?_ z&}Hdzba}c0U6HOtSEj4bRp}7A8eN?ZrEAbN=~{Gcx(;2Ju1D9W8_*5uMs#Dk3Eh-# zMmML!=oWN1-I8uax27ZLHgsD$l8&OI>2`E`x&s|U$I>0?PIMd{PbbicbZ5E?-IeY} zcc**MJ?UO_Z@LfNm+nXRrw7mj=|S{hdI&v~9!3wRN6<<1NO}}KnjS-srN`0Z>128W zJ&~S7Po}5PQ|W2+bb1Colb%J-rsvRe>3Q^gdI7zVUPLdZm(WY;W%P1-1-+79MX#pU z&}->+^m=*&y^-ETZ>G13#Hm`T%{9K13g;kI+ZyWAt(Q z1bvb|MW3e6&}Zp$^m+OMeUZLIU#73nSLti?b@~Qq>3j5j`T_lrendZ} zpU_X~XY_OW1^tqKMZc!s&~NE?^n3aP{gM7ef2P0CU+Hi3clrnYlm0~q;8Zv@PJ;t+ zS{#Ja;q*8I&WJN%3Ihx=!ZgO1!7S!5j|mpAh$Spz1*=%YIySJ0Eo@^4yV%1%4#t^r z7MvAl!`X2ToD=85xp5wx7w5zIaRFQq7s7>c5nL1(!^Lq4ToRYUrEwWt7MH{2aRpov zSHhKX6dqa6AGh;gNV09*xJ~v3MLFkCX8PJP}XAlkpTh6;H#{@eDi@&%(3u z96T4#!}IY1ybv$Ki}4b?6feWe@d~^WufnVG8oU;-!|U+|yb*80oADOB6>r1a@eaHb z@4~zB9=sRt!~5|8d=MYPhw%}76d%LK@d!{_k@d=X#5m+=*R6<@>G z@eO=?9efwx!}sw6{189FkMR@y6hFhy@eBMCzrwHa8~hf(!|(A2{1Jb`pYa#` z6@SCu@elkH|H1)GDke3Ph6!ZSGC@o_COwmZ$;f14C1rpG9KeI!Axc*3zL<}#$;!5FgclAOl~F*lb6ZI3Gnbji%x4xb3z}L)z2bn|6Vde;PlsU#6XHGCDnN!SZ<_vR|Imeu5E-)9FOUz~F3UigY#$0D^FgKZ7 z%x&flbCsvE|tcY(=&bTbZrGR%JujYHW2jl&!(mWNWdt**a`p zwjNubZNN5U8?lYqCTvr-8QYu*&*yub{IRH9l<8C zBiT{xXm$)cmL12AXOr0p>_m1FJDHurPGzUD)7cs9Om-GKo1MeXW#_T;*#+!Eb`iUn zUBWJ9m$A#)73@lO6}y^U!>(o5vFq6l>_&DIyP4g>Ze_Qz+u0rLPIec&o880iW%sfB z*#qoB_7HoRJ;EMkkFm$u6YNR$6nmOI!=7c&vFF(f>_zqxdzrn$US+Sb*V!BFP4*Uh zo4v!{W$&@~*$3=H_7VG-eZoFvpRv!`7wk*+75kcf!@gzTvG3Ur>__$!`%d|ZC609TMJ#1-a> za7DRdTyd@hSCT8mmFCKDWw~-(d9DIik*ma2=BjX2xe%@zSDg#xYH&5VT3l_e4p*0} z$JOT=a1FUeTw|^Y*OY6&Nxy25)HnYq@pYdTs-^k=w*==C*KKxozBbZU?uM+r{nX_HcW-ecXQT z0C$i(#2x02a7Vdg+;Q#%cal5Bo#xJPXSs9SdF}#tk-NlQ=B{v8xog~Y?gn?0yT#q+ z?r?Xxd)$5Q0r!x5#69Moa8J2s+;i>)_mX?Xz2@FooG-zb5Kc8Q~FXR{Ti}@w| zQhph~oL|AOl`8E7nejUG_-@tF=H}RYKE&Nt~8^4|3!SCdE@w@pw{9b+^zn?$A zALI}5hxsG?QT`ZzoIk;z@wfRq z{9XPYf1iKAKja_rkNGG3Q~nwMoPWW;gE z!T;oc@c|?iNlns_K$4aOk#rt^+^NLkTfEVNfXkP zG$YMP7->PmNlVg-v?dXx4QWdvNfe1D?MQplfy9tl(vfr`aU`B3kVMj%bRk_yH`1N- zAU#Ph(wp=leMvvkp9~-a$sjVA3?W0wFfyEsAW38-8AV2uF=Q+mN5+$6GJ#AalgMN; zg-j*W$aFG;%p|kOY%+(;CG*I9vVbfki^yWKge)b?$a1oRtR$<*YO;o`CF{s~vVm+Q zo5*Ieg={6;$ab=W>?FI$ZnB5$CHu&Ja)2Bphsa@agd8Qu$Z>LloFu2nX>x{~CFjU_ za)DeVm&j#ugf+Wa-BB+8U=z<}bf+g63Be;Sm_(HIdS;!(}6|xE0g&aaoA(xO_ z$Rp$x@(KBc0zyHdkWg4CA`}&h3B`pHLP?>NP+BM>loiSe<%J4DMWK>VS*RjZ6+(n+ zLUkcjs3Fu8Y6-Q4IznBco={(CAT$&j35|s&LQ|oc&|C-;S_t7nOQDs}T8I$Z2yKN( zAxelA+6nE24nm9&D|8e(32{QakRT)qorNw!SD~BGUFae76nY80g+4-Gp`XxS7$6K3 z1_^_OA;M5$m@r%zAtVVSg;Bz2VT>?V7$=Mul7$JvL}8LJS(qYB6{ZQ(g&D$3VU{pk zm?O*;<_YtK1;RpMk+4`;A}ke_3Co2Q!b)M4uv%CntQFP?>xB)%Mq!h%S=b_M6}Ac6 zg&o39VVAI5*dy!}_6hri1HwV!kZ@QyA{-Tt3CD#K!b#zja9TJcoE6Rq=Ye}B0LqI3D1QW!b{+&X~aM=tr#Sx6Vr2onv5VMM>?U>>dx$;7USe;tkJwl2C-xTyhy%qz z;$U%zI8+=a4i`s=N#aOxlsH-(BaRiviQ~m&ae_EeoFq;br-)OXqm)UaBp^WvNwkC| zMq(vS;w2&pk|;@%EGd#IX_77(k||k|Ejf}ad6F*$OPQrCQdTLOlwHap<&<(sxurZ( zUMZiHUn(FKlnP0Or6N*MshCt;Dj}7WN=c=qGE!NooK#+_AXStqNtLB4QdKELswP#J zLZuo~O{tbtTdE_~mFh|Lr3O+%sgcxJY9ck2nn}&2FsX$UF13_eNv)*_sg2ZDij<5!sgo2Z#Y+iNqSRUHB6XFzN!_I$QctOu)LZH!^_BWb{iOlYKxvRP zSQ;V?m4->fr4dq+G*TKRjh4npW2JG@cqv($AWf7eNt2~1(o|`hG+mk@&6H+Iv!yxG zTxp&(Us@n7lom;gr6tl*X_>TKS|P2JR!OU+HPTvXowQ!sAZ?U3Nt>lD(pG7kv|ZXE z?UZ&&yQMwSUTL4SUpgQilnzOUr6bZ&>6mm}Iw75uPD!VwGtyb6!FgdLg}(UP-T|H_}__o%CM% zAbpfRNuQ-J(pTx5^j-QP{gi%50dguiwVXx{l+(&VaymJ^oI%bgXObxy$WTTyEn}IH zS(%f0naF}H%91S0imb|-tjmUM%9d=)j_k^w?90J&W;u(TRn8`7mvhKD-ZIggxI z&L`)W3&;iKLULibh+I@ICKs1W$R*`ca%s7YTvje8mzOKZ73E5DWx0x6RSuD>$<^gh zxrSU*t|ix&>&SKGdUAcaf!t7TBsZ3u$W7&Da&tLMZXt)uE#+2nYdJ!0Be#_!tr@@x5x{8oM^zn4GA zALUQ-XZef#RsJS_mw(7V97;|lmy%n_qvTcc zDfyKGNLX>JsbtP1( zq104rDYcb4N?oO%QeSDHG*lWXjg=-!Q>B^GTnSTJDB(&=rIpfJiBQ@oZIwtRN{Lq5 zDeaXGN{kY#bW}PiaZ0?Bpd>1tl`cwGrJK@S>7n#gdMUk?K1yGupVD6$pbS(7DT9?E z%1~vPGF%y9Ws|a5*`jP!wkg|{9m-B+ zm$F;gqwH1oDf^WJ%0cCja#%T{9951f$CVSxN#&GsS~;VfRn95rl?%#6<&tt)xuRTE zt|`}*8_G@PmU3IUquf>QDfg8J%0uOm@>qGIJXM}4&y^R-OXZdFT6v?qRo*G@l@H2C z<&*MR`J#MPzA4|8AIeYVmlB|+Qd6sG)Ic?@8lsG=&VvZ|=6s;RnasHSSEw(6*^>Z!gOtY%iTs9Du)YIZe;np4fC=2r8ldDVPs zezkyFP%WeuR*R@b)naOKwS-zyEv1%L%cy15a%y?Cf?83nq*hj|s8!VvwVGO84OMHX zHPu>bZMBYCSFNYkR~x7e)kbP#wTaqPZKgI?!_*dPxY|-}rM6Zh)HZ5cHByaIqt$k5 zd$ofaqsFQo)lO=h8m}g(iE3xHi`rG~rgm3*s6EwQYHziV+E?wT_E!g}1Jyz5V0DN( zR2`-cS4XHx>PU5zI$9m0j#bC0U4F6I#Zpc&Q|BBbJcn3 ze071kP+g=hR+p$t)n)2(b%nZ8U8Sy8*QjgNb?SO`gSt`Oq;6KXs9V)->UMR9x>Mby z?pF7xd)0mFe)WKQP(7p`R*$Gh)nn>$^@Ms-J*A#j&!}hBbLx5Zf_hQCq+V99s8`i% z>UH&odQ-in-d69Zch!69ef5F*P<^C6R-dR()o1E+^@aLUeWkuu->7fZcj|lfgZfeZ zq<&Vvs9)7@>UZ^r`cwU-2570Y)LI%XP)n-?Y3a1|S_UnnmPw;Dpg|33w1zcCV>M3W zHKGZcs7acvDVnNjnywj|sacw>Ihw0^ny&?GnYAohRxO*BUCW{6)N*OLwLDs0EuWTO zE1(tB3TcJ4B3etE1J`>S^`023kX{k=9siqBYf;Y0b4Tt%Vk@wbWW^t+fcPjn-C+)S|R#t)13h z>!8JGv06v1lNP7NYYAGS)>-SKb=A6Q-L)QCPpy~MTkE6s)%t1uwE@~dZICut8=?)> zhH1mK5n7TqQX8d>*2ZXKwQ<^bEm@nOP1Gi7leH<@RBf6zU7MlJ)Mjb3wK>{cZJst? zTc9n}7HNyMCE8MLnYLV8p{>+bX{)t0+FEU$wqDzyZPYeto3$<4R&ATMUE87U)OKmR zwLRKiZJ)MZJD?rZ4rzzABid2zn08z{p`FxDX{WU_+F9+Kc3!)nUDPgVm$fU}RqdK~ zUAv*()NX0FwL98f?VfgDd!Rkk9%+xYC)!i(nf6?Jp}o{zX|J_6+FR|N_Fns-ebhc_ zpS3UASM8hjUHhT^)P89JdMZ7&o<gX(kUJ2P)9ngW1Z1iozr=p z=z=clk}m6tuIieu>xORXmTv2g?&_ZI>%n?vJ&T@I&!%VBbLctsTzYOjkDgc0r{~uT z=mqscdSShYUQ{op7uQSZCG}EzX}yeIRxhWQ*DL51^-6kWy^3B{57DdX)%8%lhF(*z zrPtQ$=ymmadVRft-cWC(H`bfzP4#Aab3IINp@-`&^;UXoJwk7zx78!{C_P$lr?=NT z=rMY%-cj$Q$LaBUf}W^%*1PCk^=^81y@%dY@1^(F`{;f3etLg>fId(kqz~4I=tK2k z`fz=Oo}`b|N9m*WG5T12oIYMp)+gu_^-20FzEoePFV|P-EA>_SYJH8qR$r&D*Ei@J^-cO_eT%+T-==TZcj!CyUHWc) zkG@ymr|;Jf=m+&f`eFTuepElEAJR)43z*FWeV^-ua| z{fqup|E7P}f9OB;UwVL%%1CXbF#?UWMv#%tNN;2?G8&l-$^Zs5kU<;RU<}sa4BjAy zV2Flf$cAF5hGyu7VVH(x*oI@chG+Oju#wrwVq`V48QF~-MouG_k=w{)^1fo`;7y}LF15d*f?SwHI5m_jT6R66otRnZ6lpW;U~!S?GNOP1q+8kq!HOHCb&17?eInkVCPBy2QQ_X4SbaRF|)0}0_Hs_dg&3Wd0bAh?g zTx2dbmzYbTi9YO*5WMQB9>r@ zmSoA6VyTv9>6T%cmSx$NW4V@R`Bt!%*~(&NwX#{+tsGWPE0>kq%46lV@>%(<0#-q* zkX6_!VimQDS;egqR!OUrRoW_Jm9@%Q<*f=kv!U^TQFS&gkGR#U5))!YiRT3F#$ORJUD+KRB+SZ%FHE6R$t+F9+b4pxj6 zYjw0bS#egpm0%@WovkibSF4-V-RfcWw0c>+tv*&?tDn{18ek2y23doxA=Xf9m^IuQ zVI^53tx?u!Ym7D48fT5SlC25WL~D{Y*_vWawWe9qtr^x#YnC#YseMr)I`+1g@lwYFK?tsT}*YnQd#+GFjt z_F4O_1J*(7kagHPVjZ=PS;wst)=BG>b=o>(owd$c=dBCYMeCAv*}7s~wXRv$tsB-& z>y~xfx?|n7?pgP(2i8ODk@eVmVm-BlOb34p#VTao-?N)YcJHl>bx3wefC_CD2XScUI*fDmj z-O=u3$Jz0Af}Lo0w!7F}?QV8=yNBJ=?q&D3``CT$es+I*fIZM2WDmB7*hB4M_HcWI zon()+N70&XV146*bD7N z_F{X9z0_W2FSl3NEA3VGYI}{n)?R0?w>Q`u?M?P(dyBo*-ezyNci21aUG{E!kGjXLJob*lxC!>?ep&Z~q2RXEZ9mZiD&fy*62#)AT zj_fFo>S&Jc7>?;!j_o*(>v)du1Us3XEKXJ@o0Hwi;pB93Ik}xYPF^RUliw-e6m$wX zg`FZ!QKy(w+$rIdbV@m;oia{Yr<_yXso+#}Dmj&%Do#}=#Hr>~cS4;SPEDtlQ`@QI z)OG4P^_>PzL#L6`*lFT4b(%TNoiL|`6YjKhS~;zq2&awH)`@hYoM@+=)86Ue#5l1| zN2ik$=fpb+PNLJ<>Ed*Cx;fpQ9!^iEm($znQ)4LhmjBX~Ea)Aq7$$!g>}GbexLMt7Zgw|^o72tZ=63VAdEI<&ez$;I&@JQ^ zc8j=0-C}NWw}e~LE#;PW%eZCTa&CFIf?Lt8w~5=-ZRR$2!`v2bxZBcg<+gSs+%|4oH`0xAquq9Hd$)rd{xKrI}?sRvCJJX%z&UWXxbKQCFe0PDn&|Ty% zc9*zI-DU1_cZIvsUFEKJ*SKrlb?$n1gS*k){Nu&$wsZbMATff_u@um(T=t-XJ zDW2+Sp6(f*=~;FnY}DtRxg{E-OJ(S^m2K*y*yrCFQ1p+E8rFM3VDUS zB3@Cim{;5@;g$4Cd8NHFURkf4SKh1ORrD%(mAxunRWHP==2iDXy&7Ikua;NatK-%6 z>Us6O23|w2k=NL3;x+Y}dCk2ruZ0)xwe(tft-T1Zjn~$T^rF0IubtQ4>)^$Bv0g{7 zlNaa3dkJ2m*V*ghb@jS=-Mt=OPp_BP+w0@?_4;}Jy#d}pZ;&_G8{!T1hIzxi5nhrv z(i`QC_QrT)y>Z@nFWH;mP4p&tlf5b4RBxI$-J9Xf^k#Xpy*b`oZ=N^bTi`A97I}-k zCEikRnYY|q;jQ#md8@rO-db;+x8B>}ZS*#Io4qaGR&SfP-P_^q^mci>y*=JuZ=bi{ zJK!Dk4ta;YBi>Q(n0MSe;hpqOd8fTI-dXRQciy|;UGy$_m%S_ARqvX2-Miu4^lo{# zy*u7r@1A$xd*D6v9(j+wC*D)^MSMQtm z-TUGF^nQ5(ekwn;pT-aL)A~VvIzPRi!O!St@+lwq&__P)W1sO^pYwU2_<}F`k}vy; zulky=`-X4&mT&ux@A{ta`@w!@KZ~E$&*o?MbND&^Tz+mpkDu4i=jZne_yzq!eqq0e zU(_$=7xzo}CH+!_;vky zeto}z-_UR5H};$OP5owmb3e>);fMPz{Z@W!Kf-V0xAi0aC_mb7=ePGe_%VL0-_h^n z$NBMof}iMj_Ph99{ce7DzlYz`@8$RQ`}lqRetv&{fIrY5S_zV3-{$hWL zztmsmFZWmYEB#geYJZKt)?eqZ_c!<({Z0O6e~Z7>-{x=kclbN~UH)!=kH6R7=kNCq z_y_$%{$c-!f7CzbANNoAC;e0YY5$CW)<5T;_b>Pt{Y(C3|B8Rrzvf@}Z}>O;TmEhT zj(^v`=im1q_z(R@{$u}%|I~lxKlfkwFa1~kYyXY^)_>=}_doa_{ZIa9|BL_C|K@-9 zfA~NBUw%Mvs^HYYX@UcT(*_3xrwdLWoFO=4aHe1?7zD#$6#RRrT(9II$t@G3p9BU3 z^h(a06ciI1*CD)pa%8{E!_x-@*~WyD-knwao}VJt_tgDcD$a@^!J;m}b(BBwI8Oy{+h3Bdp+pb$|a`iF2e@CkNKamoX(#5uE6CaTf zpZvQG8Ow(!MAZ0WNRHeUJv(Kj-uUD!{r>pUh5bb)Cy~}EsKkV*_BrDs+J`4Zb&hBq z(LNzOISKsl`rnVH?G_Op7oQxGDk-Q#Y)nFAiW-m<*gCx1A0>VJi13d8EYe2CCjN#7 zB&F*R6_c0{@mG(JXcZgN`j1X^h-wuV`(K8@-|WeuNooIZrzmMs$Wlb$Z>kg-^p_|( zH2PiYfPm1XO#elY;!f8kBBI^jCo+UZhR25`M0JQrNg%Oh5)+mf9})JyMGcK!n$ne& zQc}hIE-1s_jT6;9A~|I=VG{aJ1H&T2V&apd=cJ(jb})K=iWKkGk40ny8T^94jN z|0`Df6)XRWRe#0mzhcc_u{K47Mz8yGU;pR6;kP3wdgGtPpDLp_{n0o7xo`RHPL>!-&Yd#53us@hDYxDKZfxsGa7@2M#rZ-m=yS@x#N?|1!ed%;4%648A%!5 zcswO$xgZ388~?HWD^g0d-%Fs$og#s>jBd72~3k zvxNSW__zozjiFL_u_{()xv zU)KL#{(=2I$Vut{fPQoR_t8zFY9)rpB}ByiJE#9O{0)=lZMpA}GDTn0WjQ_RzOPA^oUAo_(f7SMH{V#y}0|;!8a#ns<{#W_q3jO2B@Y@qs zH! Date: Fri, 27 May 2022 00:35:04 +0800 Subject: [PATCH 59/60] resolve warning --- pandas/_libs/interval.pyx | 4 ++-- pandas/tests/dtypes/test_dtypes.py | 10 ---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index afbb81a48541c..54a13f498d0c4 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -217,12 +217,12 @@ def _warning_interval(inclusive: str | None = None, closed: lib.NoDefault = lib. """ warning in interval class for variable inclusive and closed """ - if inclusive is not None and not isinstance(closed, lib.NoDefault): + 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 not isinstance(closed, lib.NoDefault): + elif closed != lib.no_default: warnings.warn( "Argument `closed` is deprecated in favor of `inclusive`.", FutureWarning, diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index a39f4f6200261..b7de8016f8fac 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -1,4 +1,3 @@ -import pickle import re import numpy as np @@ -837,15 +836,6 @@ def test_interval_dtype_error_and_warning(self): ): IntervalDtype("int64", closed="right") - def test_interval_dtype_with_pickle(self): - i = IntervalDtype(subtype="int64", inclusive="right") - dumps_i = pickle.dumps(i) - loads_i = pickle.loads(dumps_i) - - assert loads_i.subtype == np.dtype("int64") - assert loads_i.inclusive == "right" - assert is_interval_dtype(loads_i) - class TestCategoricalDtypeParametrized: @pytest.mark.parametrize( From 41f59fb15f8f75b032c72a355aee39930bdaf495 Mon Sep 17 00:00:00 2001 From: weikhor Date: Sat, 28 May 2022 11:33:05 +0800 Subject: [PATCH 60/60] mypy --- pandas/_libs/interval.pyx | 4 ++-- pandas/_libs/intervaltree.pxi.in | 2 +- pandas/core/arrays/arrow/_arrow_utils.py | 2 +- pandas/core/arrays/interval.py | 4 ++-- pandas/core/dtypes/dtypes.py | 2 +- pandas/core/indexes/interval.py | 8 ++++---- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 54a13f498d0c4..178836ff1548b 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -213,7 +213,7 @@ cdef bint _interval_like(other): and hasattr(other, 'right') and hasattr(other, 'inclusive')) -def _warning_interval(inclusive: str | None = None, closed: lib.NoDefault = lib.no_default): +def _warning_interval(inclusive: str | None = None, closed: None | lib.NoDefault = lib.no_default): """ warning in interval class for variable inclusive and closed """ @@ -354,7 +354,7 @@ cdef class Interval(IntervalMixin): neither. """ - def __init__(self, left, right, inclusive: str | None = None, closed: lib.NoDefault = lib.no_default): + 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 diff --git a/pandas/_libs/intervaltree.pxi.in b/pandas/_libs/intervaltree.pxi.in index 7722e9d01a3d2..51db5f1e76c99 100644 --- a/pandas/_libs/intervaltree.pxi.in +++ b/pandas/_libs/intervaltree.pxi.in @@ -42,7 +42,7 @@ cdef class IntervalTree(IntervalMixin): object _is_overlapping, _left_sorter, _right_sorter Py_ssize_t _na_count - def __init__(self, left, right, inclusive: str | None = None, closed: lib.NoDefault = lib.no_default, leaf_size=100): + def __init__(self, left, right, inclusive: str | None = None, closed: None | lib.NoDefault = lib.no_default, leaf_size=100): """ Parameters ---------- diff --git a/pandas/core/arrays/arrow/_arrow_utils.py b/pandas/core/arrays/arrow/_arrow_utils.py index 8e451bbf26f86..e0f242e2ced5d 100644 --- a/pandas/core/arrays/arrow/_arrow_utils.py +++ b/pandas/core/arrays/arrow/_arrow_utils.py @@ -109,7 +109,7 @@ def __init__( self, subtype, inclusive: str | None = None, - closed: lib.NoDefault = lib.no_default, + closed: None | lib.NoDefault = lib.no_default, ) -> None: # attributes need to be set first before calling # super init (as that calls serialize) diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index c6bdcd89bd9a5..eecf1dff4dd48 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -220,7 +220,7 @@ def __new__( cls: type[IntervalArrayT], data, inclusive: str | None = None, - closed: lib.NoDefault = lib.no_default, + closed: None | lib.NoDefault = lib.no_default, dtype: Dtype | None = None, copy: bool = False, verify_integrity: bool = True, @@ -268,7 +268,7 @@ def _simple_new( left, right, inclusive=None, - closed: lib.NoDefault = lib.no_default, + closed: None | lib.NoDefault = lib.no_default, copy: bool = False, dtype: Dtype | None = None, verify_integrity: bool = True, diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index d664129c3973a..64d46976b54f6 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -1069,7 +1069,7 @@ def __new__( cls, subtype=None, inclusive: str_type | None = None, - closed: lib.NoDefault = lib.no_default, + closed: None | lib.NoDefault = lib.no_default, ): from pandas.core.dtypes.common import ( is_string_dtype, diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index a024dd5218e4a..11e2da47c5738 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -213,7 +213,7 @@ def __new__( cls, data, inclusive=None, - closed: lib.NoDefault = lib.no_default, + closed: None | lib.NoDefault = lib.no_default, dtype: Dtype | None = None, copy: bool = False, name: Hashable = None, @@ -255,7 +255,7 @@ def from_breaks( cls, breaks, inclusive=None, - closed: lib.NoDefault = lib.no_default, + closed: None | lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, @@ -292,7 +292,7 @@ def from_arrays( left, right, inclusive=None, - closed: lib.NoDefault = lib.no_default, + closed: None | lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None, @@ -328,7 +328,7 @@ def from_tuples( cls, data, inclusive=None, - closed: lib.NoDefault = lib.no_default, + closed: None | lib.NoDefault = lib.no_default, name: Hashable = None, copy: bool = False, dtype: Dtype | None = None,