Skip to content

ENH: Implement is_empty property for Interval structures #27221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/source/reference/arrays.rst
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ Properties
Interval.closed
Interval.closed_left
Interval.closed_right
Interval.is_empty
Interval.left
Interval.length
Interval.mid
Expand Down Expand Up @@ -331,6 +332,7 @@ A collection of intervals may be stored in an :class:`arrays.IntervalArray`.
arrays.IntervalArray.closed
arrays.IntervalArray.mid
arrays.IntervalArray.length
arrays.IntervalArray.is_empty
arrays.IntervalArray.is_non_overlapping_monotonic
arrays.IntervalArray.from_arrays
arrays.IntervalArray.from_tuples
Expand Down
1 change: 1 addition & 0 deletions doc/source/reference/indexing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ IntervalIndex components
IntervalIndex.closed
IntervalIndex.length
IntervalIndex.values
IntervalIndex.is_empty
IntervalIndex.is_non_overlapping_monotonic
IntervalIndex.is_overlapping
IntervalIndex.get_loc
Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.25.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ Other enhancements
- :class:`pandas.offsets.BusinessHour` supports multiple opening hours intervals (:issue:`15481`)
- :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`)
- :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`)
- :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`)

.. _whatsnew_0250.api_breaking:

Expand Down
53 changes: 53 additions & 0 deletions pandas/_libs/interval.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,59 @@ cdef class IntervalMixin:
"""Return the length of the Interval"""
return self.right - self.left

@property
def is_empty(self):
"""
Indicates if an interval is empty, meaning it contains no points.

.. versionadded:: 0.25.0

Returns
-------
bool or ndarray
A boolean indicating if a scalar :class:`Interval` is empty, or a
boolean ``ndarray`` positionally indicating if an ``Interval`` in
an :class:`~arrays.IntervalArray` or :class:`IntervalIndex` is
empty.

Examples
--------
An :class:`Interval` that contains points is not empty:

>>> pd.Interval(0, 1, closed='right').is_empty
False

An ``Interval`` that does not contain any points is empty:

>>> pd.Interval(0, 0, closed='right').is_empty
True
>>> pd.Interval(0, 0, closed='left').is_empty
True
>>> pd.Interval(0, 0, closed='neither').is_empty
True

An ``Interval`` that contains a single point is not empty:

>>> pd.Interval(0, 0, closed='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')]
>>> pd.arrays.IntervalArray(ivs).is_empty
array([ True, False])

Missing values are not considered empty:

>>> ivs = [pd.Interval(0, 0, closed='neither'), np.nan]
>>> pd.IntervalIndex(ivs).is_empty
array([ True, False])
"""
return (self.right == self.left) & (self.closed != 'both')

def _check_closed_matches(self, other, name='other'):
"""Check if the closed attribute of `other` matches.

Expand Down
1 change: 1 addition & 0 deletions pandas/core/arrays/interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
closed
mid
length
is_empty
is_non_overlapping_monotonic
%(extra_attributes)s\

Expand Down
21 changes: 20 additions & 1 deletion pandas/tests/arrays/interval/test_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import pytest

import pandas as pd
from pandas import Index, Interval, IntervalIndex, date_range, timedelta_range
from pandas import (
Index, Interval, IntervalIndex, Timedelta, Timestamp, date_range,
timedelta_range)
from pandas.core.arrays import IntervalArray
import pandas.util.testing as tm

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


class TestAttributes:
@pytest.mark.parametrize('left, right', [
(0, 1),
(Timedelta('0 days'), Timedelta('1 day')),
(Timestamp('2018-01-01'), Timestamp('2018-01-02')),
pytest.param(Timestamp('2018-01-01', tz='US/Eastern'),
Timestamp('2018-01-02', tz='US/Eastern'),
marks=pytest.mark.xfail(strict=True, reason='GH 27011'))])
@pytest.mark.parametrize('constructor', [IntervalArray, IntervalIndex])
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
tm.assert_numpy_array_equal(result, expected)


class TestMethods:

@pytest.mark.parametrize('new_closed', [
Expand Down
18 changes: 18 additions & 0 deletions pandas/tests/scalar/interval/test_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,24 @@ def test_length_timestamp(self, tz, left, right, expected):
expected = Timedelta(expected)
assert result == expected

@pytest.mark.parametrize('left, right', [
(0, 1),
(Timedelta('0 days'), Timedelta('1 day')),
(Timestamp('2018-01-01'), Timestamp('2018-01-02')),
(Timestamp('2018-01-01', tz='US/Eastern'),
Timestamp('2018-01-02', tz='US/Eastern'))])
def test_is_empty(self, left, right, closed):
# GH27219
# non-empty always return False
iv = Interval(left, right, closed)
assert iv.is_empty is False

# same endpoint is empty except when closed='both' (contains one point)
iv = Interval(left, left, closed)
result = iv.is_empty
expected = closed != 'both'
assert result is expected

@pytest.mark.parametrize('left, right', [
('a', 'z'),
(('a', 'b'), ('c', 'd')),
Expand Down