Skip to content

Commit 1be80c9

Browse files
jschendelTomAugspurger
authored andcommitted
ENH: Implement is_empty property for Interval structures (#27221)
* ENH: Implement is_empty property for Interval structures
1 parent 96c7ab5 commit 1be80c9

File tree

7 files changed

+96
-1
lines changed

7 files changed

+96
-1
lines changed

doc/source/reference/arrays.rst

+2
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ Properties
295295
Interval.closed
296296
Interval.closed_left
297297
Interval.closed_right
298+
Interval.is_empty
298299
Interval.left
299300
Interval.length
300301
Interval.mid
@@ -331,6 +332,7 @@ A collection of intervals may be stored in an :class:`arrays.IntervalArray`.
331332
arrays.IntervalArray.closed
332333
arrays.IntervalArray.mid
333334
arrays.IntervalArray.length
335+
arrays.IntervalArray.is_empty
334336
arrays.IntervalArray.is_non_overlapping_monotonic
335337
arrays.IntervalArray.from_arrays
336338
arrays.IntervalArray.from_tuples

doc/source/reference/indexing.rst

+1
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ IntervalIndex components
254254
IntervalIndex.closed
255255
IntervalIndex.length
256256
IntervalIndex.values
257+
IntervalIndex.is_empty
257258
IntervalIndex.is_non_overlapping_monotonic
258259
IntervalIndex.is_overlapping
259260
IntervalIndex.get_loc

doc/source/whatsnew/v0.25.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ Other enhancements
212212
- :class:`pandas.offsets.BusinessHour` supports multiple opening hours intervals (:issue:`15481`)
213213
- :func:`read_excel` can now use ``openpyxl`` to read Excel files via the ``engine='openpyxl'`` argument. This will become the default in a future release (:issue:`11499`)
214214
- :func:`pandas.io.excel.read_excel` supports reading OpenDocument tables. Specify ``engine='odf'`` to enable. Consult the :ref:`IO User Guide <io.ods>` for more details (:issue:`9070`)
215+
- :class:`Interval`, :class:`IntervalIndex`, and :class:`~arrays.IntervalArray` have gained an :attr:`~Interval.is_empty` attribute denoting if the given interval(s) are empty (:issue:`27219`)
215216

216217
.. _whatsnew_0250.api_breaking:
217218

pandas/_libs/interval.pyx

+53
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,59 @@ cdef class IntervalMixin:
107107
"""Return the length of the Interval"""
108108
return self.right - self.left
109109

110+
@property
111+
def is_empty(self):
112+
"""
113+
Indicates if an interval is empty, meaning it contains no points.
114+
115+
.. versionadded:: 0.25.0
116+
117+
Returns
118+
-------
119+
bool or ndarray
120+
A boolean indicating if a scalar :class:`Interval` is empty, or a
121+
boolean ``ndarray`` positionally indicating if an ``Interval`` in
122+
an :class:`~arrays.IntervalArray` or :class:`IntervalIndex` is
123+
empty.
124+
125+
Examples
126+
--------
127+
An :class:`Interval` that contains points is not empty:
128+
129+
>>> pd.Interval(0, 1, closed='right').is_empty
130+
False
131+
132+
An ``Interval`` that does not contain any points is empty:
133+
134+
>>> pd.Interval(0, 0, closed='right').is_empty
135+
True
136+
>>> pd.Interval(0, 0, closed='left').is_empty
137+
True
138+
>>> pd.Interval(0, 0, closed='neither').is_empty
139+
True
140+
141+
An ``Interval`` that contains a single point is not empty:
142+
143+
>>> pd.Interval(0, 0, closed='both').is_empty
144+
False
145+
146+
An :class:`~arrays.IntervalArray` or :class:`IntervalIndex` returns a
147+
boolean ``ndarray`` positionally indicating if an ``Interval`` is
148+
empty:
149+
150+
>>> ivs = [pd.Interval(0, 0, closed='neither'),
151+
... pd.Interval(1, 2, closed='neither')]
152+
>>> pd.arrays.IntervalArray(ivs).is_empty
153+
array([ True, False])
154+
155+
Missing values are not considered empty:
156+
157+
>>> ivs = [pd.Interval(0, 0, closed='neither'), np.nan]
158+
>>> pd.IntervalIndex(ivs).is_empty
159+
array([ True, False])
160+
"""
161+
return (self.right == self.left) & (self.closed != 'both')
162+
110163
def _check_closed_matches(self, other, name='other'):
111164
"""Check if the closed attribute of `other` matches.
112165

pandas/core/arrays/interval.py

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
closed
6767
mid
6868
length
69+
is_empty
6970
is_non_overlapping_monotonic
7071
%(extra_attributes)s\
7172

pandas/tests/arrays/interval/test_interval.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import pytest
33

44
import pandas as pd
5-
from pandas import Index, Interval, IntervalIndex, date_range, timedelta_range
5+
from pandas import (
6+
Index, Interval, IntervalIndex, Timedelta, Timestamp, date_range,
7+
timedelta_range)
68
from pandas.core.arrays import IntervalArray
79
import pandas.util.testing as tm
810

@@ -23,6 +25,23 @@ def left_right_dtypes(request):
2325
return request.param
2426

2527

28+
class TestAttributes:
29+
@pytest.mark.parametrize('left, right', [
30+
(0, 1),
31+
(Timedelta('0 days'), Timedelta('1 day')),
32+
(Timestamp('2018-01-01'), Timestamp('2018-01-02')),
33+
pytest.param(Timestamp('2018-01-01', tz='US/Eastern'),
34+
Timestamp('2018-01-02', tz='US/Eastern'),
35+
marks=pytest.mark.xfail(strict=True, reason='GH 27011'))])
36+
@pytest.mark.parametrize('constructor', [IntervalArray, IntervalIndex])
37+
def test_is_empty(self, constructor, left, right, closed):
38+
# GH27219
39+
tuples = [(left, left), (left, right), np.nan]
40+
expected = np.array([closed != 'both', False, False])
41+
result = constructor.from_tuples(tuples, closed=closed).is_empty
42+
tm.assert_numpy_array_equal(result, expected)
43+
44+
2645
class TestMethods:
2746

2847
@pytest.mark.parametrize('new_closed', [

pandas/tests/scalar/interval/test_interval.py

+18
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,24 @@ def test_length_timestamp(self, tz, left, right, expected):
9494
expected = Timedelta(expected)
9595
assert result == expected
9696

97+
@pytest.mark.parametrize('left, right', [
98+
(0, 1),
99+
(Timedelta('0 days'), Timedelta('1 day')),
100+
(Timestamp('2018-01-01'), Timestamp('2018-01-02')),
101+
(Timestamp('2018-01-01', tz='US/Eastern'),
102+
Timestamp('2018-01-02', tz='US/Eastern'))])
103+
def test_is_empty(self, left, right, closed):
104+
# GH27219
105+
# non-empty always return False
106+
iv = Interval(left, right, closed)
107+
assert iv.is_empty is False
108+
109+
# same endpoint is empty except when closed='both' (contains one point)
110+
iv = Interval(left, left, closed)
111+
result = iv.is_empty
112+
expected = closed != 'both'
113+
assert result is expected
114+
97115
@pytest.mark.parametrize('left, right', [
98116
('a', 'z'),
99117
(('a', 'b'), ('c', 'd')),

0 commit comments

Comments
 (0)