Skip to content

Commit fe900cb

Browse files
jschendeljreback
authored andcommitted
BUG: Fix IntervalIndex.to_tuples() with NA values (#18757)
1 parent 7b4229a commit fe900cb

File tree

3 files changed

+66
-3
lines changed

3 files changed

+66
-3
lines changed

doc/source/whatsnew/v0.22.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Other Enhancements
138138
- :func:`Series` / :func:`DataFrame` tab completion also returns identifiers in the first level of a :func:`MultiIndex`. (:issue:`16326`)
139139
- :func:`read_excel()` has gained the ``nrows`` parameter (:issue:`16645`)
140140
- :func:``DataFrame.to_json`` and ``Series.to_json`` now accept an ``index`` argument which allows the user to exclude the index from the JSON output (:issue:`17394`)
141+
- ``IntervalIndex.to_tuples()`` has gained the ``na_tuple`` parameter to control whether NA is returned as a tuple of NA, or NA itself (:issue:`18756`)
141142

142143
.. _whatsnew_0220.api_breaking:
143144

pandas/core/indexes/interval.py

+25-3
Original file line numberDiff line numberDiff line change
@@ -544,9 +544,31 @@ def from_tuples(cls, data, closed='right', name=None, copy=False):
544544

545545
return cls.from_arrays(left, right, closed, name=name, copy=False)
546546

547-
def to_tuples(self):
548-
"""Return an Index of tuples of the form (left, right)"""
549-
return Index(_asarray_tuplesafe(zip(self.left, self.right)))
547+
def to_tuples(self, na_tuple=True):
548+
"""
549+
Return an Index of tuples of the form (left, right)
550+
551+
Parameters
552+
----------
553+
na_tuple : boolean, default True
554+
Returns NA as a tuple if True, ``(nan, nan)``, or just as the NA
555+
value itself if False, ``nan``.
556+
557+
..versionadded:: 0.22.0
558+
559+
Examples
560+
--------
561+
>>> idx = pd.IntervalIndex.from_arrays([0, np.nan, 2], [1, np.nan, 3])
562+
>>> idx.to_tuples()
563+
Index([(0.0, 1.0), (nan, nan), (2.0, 3.0)], dtype='object')
564+
>>> idx.to_tuples(na_tuple=False)
565+
Index([(0.0, 1.0), nan, (2.0, 3.0)], dtype='object')
566+
"""
567+
tuples = _asarray_tuplesafe(zip(self.left, self.right))
568+
if not na_tuple:
569+
# GH 18756
570+
tuples = np.where(~self._isnan, tuples, np.nan)
571+
return Index(tuples)
550572

551573
@cache_readonly
552574
def _multiindex(self):

pandas/tests/indexes/test_interval.py

+40
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
Interval, IntervalIndex, Index, isna, notna, interval_range, Timestamp,
88
Timedelta, compat, date_range, timedelta_range, DateOffset)
99
from pandas.compat import lzip
10+
from pandas.core.common import _asarray_tuplesafe
1011
from pandas.tseries.offsets import Day
1112
from pandas._libs.interval import IntervalTree
1213
from pandas.tests.indexes.common import Base
@@ -1072,6 +1073,45 @@ def test_is_non_overlapping_monotonic(self, closed):
10721073
idx = IntervalIndex.from_breaks(range(4), closed=closed)
10731074
assert idx.is_non_overlapping_monotonic is True
10741075

1076+
@pytest.mark.parametrize('tuples', [
1077+
lzip(range(10), range(1, 11)),
1078+
lzip(date_range('20170101', periods=10),
1079+
date_range('20170101', periods=10)),
1080+
lzip(timedelta_range('0 days', periods=10),
1081+
timedelta_range('1 day', periods=10))])
1082+
def test_to_tuples(self, tuples):
1083+
# GH 18756
1084+
idx = IntervalIndex.from_tuples(tuples)
1085+
result = idx.to_tuples()
1086+
expected = Index(_asarray_tuplesafe(tuples))
1087+
tm.assert_index_equal(result, expected)
1088+
1089+
@pytest.mark.parametrize('tuples', [
1090+
lzip(range(10), range(1, 11)) + [np.nan],
1091+
lzip(date_range('20170101', periods=10),
1092+
date_range('20170101', periods=10)) + [np.nan],
1093+
lzip(timedelta_range('0 days', periods=10),
1094+
timedelta_range('1 day', periods=10)) + [np.nan]])
1095+
@pytest.mark.parametrize('na_tuple', [True, False])
1096+
def test_to_tuples_na(self, tuples, na_tuple):
1097+
# GH 18756
1098+
idx = IntervalIndex.from_tuples(tuples)
1099+
result = idx.to_tuples(na_tuple=na_tuple)
1100+
1101+
# check the non-NA portion
1102+
expected_notna = Index(_asarray_tuplesafe(tuples[:-1]))
1103+
result_notna = result[:-1]
1104+
tm.assert_index_equal(result_notna, expected_notna)
1105+
1106+
# check the NA portion
1107+
result_na = result[-1]
1108+
if na_tuple:
1109+
assert isinstance(result_na, tuple)
1110+
assert len(result_na) == 2
1111+
assert all(isna(x) for x in result_na)
1112+
else:
1113+
assert isna(result_na)
1114+
10751115

10761116
class TestIntervalRange(object):
10771117

0 commit comments

Comments
 (0)