diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index 050a26cc86d42..3c4625401b243 100644 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -38,7 +38,25 @@ Backwards incompatible API changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :class:`pandas.core.groupby.GroupBy.transform` now raises on invalid operation names (:issue:`27489`). -- +- :class:`pandas.core.arrays.IntervalArray` adopts a new ``__repr__`` in accordance with other array classes (:issue:`25022`) + +*pandas 0.25.x* + +.. code-block:: ipython + + In [1]: pd.arrays.IntervalArray.from_tuples([(0, 1), (2, 3)]) + Out[2]: + IntervalArray([(0, 1], (2, 3]], + closed='right', + dtype='interval[int64]') + + +*pandas 1.0.0* + +.. ipython:: python + + pd.arrays.IntervalArray.from_tuples([(0, 1), (2, 3)]) + .. _whatsnew_1000.api.other: diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 7a14d6f1b619a..1f4b76a259f00 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -129,9 +129,9 @@ ``Interval`` objects: >>> pd.arrays.IntervalArray([pd.Interval(0, 1), pd.Interval(1, 5)]) - IntervalArray([(0, 1], (1, 5]], - closed='right', - dtype='interval[int64]') + + [(0, 1], (1, 5]] + Length: 2, closed: right, dtype: interval[int64] It may also be constructed using one of the constructor methods: :meth:`IntervalArray.from_arrays`, @@ -248,9 +248,8 @@ def _from_factorized(cls, values, original): values = values.astype(original.dtype.subtype) return cls(values, closed=original.closed) - _interval_shared_docs[ - "from_breaks" - ] = """ + _interval_shared_docs["from_breaks"] = textwrap.dedent( + """ Construct an %(klass)s from an array of splits. Parameters @@ -277,24 +276,34 @@ def _from_factorized(cls, values, original): %(klass)s.from_arrays : Construct from a left and right array. %(klass)s.from_tuples : Construct from a sequence of tuples. - Examples - -------- - >>> pd.%(qualname)s.from_breaks([0, 1, 2, 3]) - %(klass)s([(0, 1], (1, 2], (2, 3]], - closed='right', - dtype='interval[int64]') + %(examples)s\ """ + ) @classmethod - @Appender(_interval_shared_docs["from_breaks"] % _shared_docs_kwargs) + @Appender( + _interval_shared_docs["from_breaks"] + % dict( + klass="IntervalArray", + examples=textwrap.dedent( + """\ + Examples + -------- + >>> pd.arrays.IntervalArray.from_breaks([0, 1, 2, 3]) + + [(0, 1], (1, 2], (2, 3]] + Length: 3, closed: right, dtype: interval[int64] + """ + ), + ) + ) def from_breaks(cls, breaks, closed="right", copy=False, dtype=None): breaks = maybe_convert_platform_interval(breaks) return cls.from_arrays(breaks[:-1], breaks[1:], closed, copy=copy, dtype=dtype) - _interval_shared_docs[ - "from_arrays" - ] = """ + _interval_shared_docs["from_arrays"] = textwrap.dedent( + """ Construct from two arrays defining the left and right bounds. Parameters @@ -340,16 +349,25 @@ def from_breaks(cls, breaks, closed="right", copy=False, dtype=None): using an unsupported type for `left` or `right`. At the moment, 'category', 'object', and 'string' subtypes are not supported. - Examples - -------- - >>> %(klass)s.from_arrays([0, 1, 2], [1, 2, 3]) - %(klass)s([(0, 1], (1, 2], (2, 3]], - closed='right', - dtype='interval[int64]') + %(examples)s\ """ + ) @classmethod - @Appender(_interval_shared_docs["from_arrays"] % _shared_docs_kwargs) + @Appender( + _interval_shared_docs["from_arrays"] + % dict( + klass="IntervalArray", + examples=textwrap.dedent( + """\ + >>> pd.arrays.IntervalArray.from_arrays([0, 1, 2], [1, 2, 3]) + + [(0, 1], (1, 2], (2, 3]] + Length: 3, closed: right, dtype: interval[int64] + """ + ), + ) + ) def from_arrays(cls, left, right, closed="right", copy=False, dtype=None): left = maybe_convert_platform_interval(left) right = maybe_convert_platform_interval(right) @@ -358,9 +376,8 @@ def from_arrays(cls, left, right, closed="right", copy=False, dtype=None): left, right, closed, copy=copy, dtype=dtype, verify_integrity=True ) - _interval_shared_docs[ - "from_tuples" - ] = """ + _interval_shared_docs["from_tuples"] = textwrap.dedent( + """ Construct an %(klass)s from an array-like of tuples. Parameters @@ -389,15 +406,27 @@ def from_arrays(cls, left, right, closed="right", copy=False, dtype=None): %(klass)s.from_breaks : Construct an %(klass)s from an array of splits. - Examples - -------- - >>> pd.%(qualname)s.from_tuples([(0, 1), (1, 2)]) - %(klass)s([(0, 1], (1, 2]], - closed='right', dtype='interval[int64]') + %(examples)s\ """ + ) @classmethod - @Appender(_interval_shared_docs["from_tuples"] % _shared_docs_kwargs) + @Appender( + _interval_shared_docs["from_tuples"] + % dict( + klass="IntervalArray", + examples=textwrap.dedent( + """\ + Examples + -------- + >>> pd.arrays.IntervalArray.from_tuples([(0, 1), (1, 2)]) + + [(0, 1], (1, 2]] + Length: 2, closed: right, dtype: interval[int64] + """ + ), + ) + ) def from_tuples(cls, data, closed="right", copy=False, dtype=None): if len(data): left, right = [], [] @@ -832,16 +861,20 @@ def _format_data(self): return summary def __repr__(self): - tpl = textwrap.dedent( - """\ - {cls}({data}, - {lead}closed='{closed}', - {lead}dtype='{dtype}')""" + template = ( + "{class_name}" + "{data}\n" + "Length: {length}, closed: {closed}, dtype: {dtype}" ) - return tpl.format( - cls=self.__class__.__name__, - data=self._format_data(), - lead=" " * len(self.__class__.__name__) + " ", + # the short repr has no trailing newline, while the truncated + # repr does. So we include a newline in our template, and strip + # any trailing newlines from format_object_summary + data = self._format_data() + class_name = "<{}>\n".format(self.__class__.__name__) + return template.format( + class_name=class_name, + data=data, + length=len(self), closed=self.closed, dtype=self.dtype, ) @@ -874,9 +907,8 @@ def closed(self): """ return self._closed - _interval_shared_docs[ - "set_closed" - ] = """ + _interval_shared_docs["set_closed"] = textwrap.dedent( + """ Return an %(klass)s identical to the current one, but closed on the specified side. @@ -892,20 +924,31 @@ def closed(self): ------- new_index : %(klass)s + %(examples)s\ + """ + ) + + @Appender( + _interval_shared_docs["set_closed"] + % dict( + klass="IntervalArray", + examples=textwrap.dedent( + """\ Examples -------- - >>> index = pd.interval_range(0, 3) + >>> index = pd.arrays.IntervalArray.from_breaks(range(4)) >>> index - IntervalIndex([(0, 1], (1, 2], (2, 3]], - closed='right', - dtype='interval[int64]') + + [(0, 1], (1, 2], (2, 3]] + Length: 3, closed: right, dtype: interval[int64] >>> index.set_closed('both') - IntervalIndex([[0, 1], [1, 2], [2, 3]], - closed='both', - dtype='interval[int64]') + + [[0, 1], [1, 2], [2, 3]] + Length: 3, closed: both, dtype: interval[int64] """ - - @Appender(_interval_shared_docs["set_closed"] % _shared_docs_kwargs) + ), + ) + ) def set_closed(self, closed): if closed not in _VALID_CLOSED: msg = "invalid option for 'closed': {closed}" @@ -1028,9 +1071,8 @@ def repeat(self, repeats, axis=None): right_repeat = self.right.repeat(repeats) return self._shallow_copy(left=left_repeat, right=right_repeat) - _interval_shared_docs[ - "contains" - ] = """ + _interval_shared_docs["contains"] = textwrap.dedent( + """ Check elementwise if the Intervals contain the value. Return a boolean mask whether the value is contained in the Intervals @@ -1055,16 +1097,27 @@ def repeat(self, repeats, axis=None): Examples -------- - >>> intervals = pd.%(qualname)s.from_tuples([(0, 1), (1, 3), (2, 4)]) - >>> intervals - %(klass)s([(0, 1], (1, 3], (2, 4]], - closed='right', - dtype='interval[int64]') + %(examples)s >>> intervals.contains(0.5) array([ True, False, False]) """ + ) - @Appender(_interval_shared_docs["contains"] % _shared_docs_kwargs) + @Appender( + _interval_shared_docs["contains"] + % dict( + klass="IntervalArray", + examples=textwrap.dedent( + """\ + >>> intervals = pd.arrays.IntervalArray.from_tuples([(0, 1), (1, 3), (2, 4)]) + >>> intervals + + [(0, 1], (1, 3], (2, 4]] + Length: 3, closed: right, dtype: interval[int64] + """ + ), + ) + ) def contains(self, other): if isinstance(other, Interval): raise NotImplementedError("contains not implemented for two intervals") @@ -1073,9 +1126,8 @@ def contains(self, other): other < self.right if self.open_right else other <= self.right ) - _interval_shared_docs[ - "overlaps" - ] = """ + _interval_shared_docs["overlaps"] = textwrap.dedent( + """ Check elementwise if an Interval overlaps the values in the %(klass)s. Two intervals overlap if they share a common point, including closed @@ -1086,7 +1138,7 @@ def contains(self, other): Parameters ---------- - other : Interval + other : %(klass)s Interval to check against for an overlap. Returns @@ -1100,11 +1152,7 @@ def contains(self, other): Examples -------- - >>> intervals = pd.%(qualname)s.from_tuples([(0, 1), (1, 3), (2, 4)]) - >>> intervals - %(klass)s([(0, 1], (1, 3], (2, 4]], - closed='right', - dtype='interval[int64]') + %(examples)s >>> intervals.overlaps(pd.Interval(0.5, 1.5)) array([ True, True, False]) @@ -1117,9 +1165,25 @@ def contains(self, other): >>> intervals.overlaps(pd.Interval(1, 2, closed='right')) array([False, True, False]) - """ + """ + ) - @Appender(_interval_shared_docs["overlaps"] % _shared_docs_kwargs) + @Appender( + _interval_shared_docs["overlaps"] + % dict( + klass="IntervalArray", + examples=textwrap.dedent( + """\ + >>> data = [(0, 1), (1, 3), (2, 4)] + >>> intervals = pd.arrays.IntervalArray.from_tuples(data) + >>> intervals + + [(0, 1], (1, 3], (2, 4]] + Length: 3, closed: right, dtype: interval[int64] + """ + ), + ) + ) def overlaps(self, other): if isinstance(other, (IntervalArray, ABCIntervalIndex)): raise NotImplementedError diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 6b0081c6a2ff5..24904adcdda80 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -250,7 +250,22 @@ def _simple_new(cls, array, name, closed=None): return result @classmethod - @Appender(_interval_shared_docs["from_breaks"] % _index_doc_kwargs) + @Appender( + _interval_shared_docs["from_breaks"] + % dict( + klass="IntervalIndex", + examples=textwrap.dedent( + """\ + Examples + -------- + >>> pd.IntervalIndex.from_breaks([0, 1, 2, 3]) + IntervalIndex([(0, 1], (1, 2], (2, 3]], + closed='right', + dtype='interval[int64]') + """ + ), + ) + ) def from_breaks(cls, breaks, closed="right", name=None, copy=False, dtype=None): with rewrite_exception("IntervalArray", cls.__name__): array = IntervalArray.from_breaks( @@ -259,7 +274,22 @@ def from_breaks(cls, breaks, closed="right", name=None, copy=False, dtype=None): return cls._simple_new(array, name=name) @classmethod - @Appender(_interval_shared_docs["from_arrays"] % _index_doc_kwargs) + @Appender( + _interval_shared_docs["from_arrays"] + % dict( + klass="IntervalIndex", + examples=textwrap.dedent( + """\ + Examples + -------- + >>> pd.IntervalIndex.from_arrays([0, 1, 2], [1, 2, 3]) + IntervalIndex([(0, 1], (1, 2], (2, 3]], + closed='right', + dtype='interval[int64]') + """ + ), + ) + ) def from_arrays( cls, left, right, closed="right", name=None, copy=False, dtype=None ): @@ -270,7 +300,22 @@ def from_arrays( return cls._simple_new(array, name=name) @classmethod - @Appender(_interval_shared_docs["from_tuples"] % _index_doc_kwargs) + @Appender( + _interval_shared_docs["from_tuples"] + % dict( + klass="IntervalIndex", + examples=textwrap.dedent( + """\ + Examples + -------- + >>> pd.IntervalIndex.from_tuples([(0, 1), (1, 2)]) + IntervalIndex([(0, 1], (1, 2]], + closed='right', + dtype='interval[int64]') + """ + ), + ) + ) def from_tuples(cls, data, closed="right", name=None, copy=False, dtype=None): with rewrite_exception("IntervalArray", cls.__name__): arr = IntervalArray.from_tuples(data, closed=closed, copy=copy, dtype=dtype) @@ -367,7 +412,27 @@ def closed(self): """ return self._data._closed - @Appender(_interval_shared_docs["set_closed"] % _index_doc_kwargs) + @Appender( + _interval_shared_docs["set_closed"] + % dict( + klass="IntervalIndex", + examples=textwrap.dedent( + """\ + Examples + -------- + >>> index = pd.interval_range(0, 3) + >>> index + IntervalIndex([(0, 1], (1, 2], (2, 3]], + closed='right', + dtype='interval[int64]') + >>> index.set_closed('both') + IntervalIndex([[0, 1], [1, 2], [2, 3]], + closed='both', + dtype='interval[int64]') + """ + ), + ) + ) def set_closed(self, closed): if closed not in _VALID_CLOSED: msg = "invalid option for 'closed': {closed}" @@ -1172,11 +1237,41 @@ def equals(self, other): and self.closed == other.closed ) - @Appender(_interval_shared_docs["contains"] % _index_doc_kwargs) + @Appender( + _interval_shared_docs["contains"] + % dict( + klass="IntervalIndex", + examples=textwrap.dedent( + """\ + >>> intervals = pd.IntervalIndex.from_tuples([(0, 1), (1, 3), (2, 4)]) + >>> intervals + IntervalIndex([(0, 1], (1, 3], (2, 4]], + closed='right', + dtype='interval[int64]') + >>> intervals.contains(0.5) + array([ True, False, False]) + """ + ), + ) + ) def contains(self, other): return self._data.contains(other) - @Appender(_interval_shared_docs["overlaps"] % _index_doc_kwargs) + @Appender( + _interval_shared_docs["overlaps"] + % dict( + klass="IntervalIndex", + examples=textwrap.dedent( + """\ + >>> intervals = pd.IntervalIndex.from_tuples([(0, 1), (1, 3), (2, 4)]) + >>> intervals + IntervalIndex([(0, 1], (1, 3], (2, 4]], + closed='right', + dtype='interval[int64]') + """ + ), + ) + ) def overlaps(self, other): return self._data.overlaps(other) diff --git a/pandas/tests/arrays/interval/test_interval.py b/pandas/tests/arrays/interval/test_interval.py index 6a86289b6fcc6..655a6e717119b 100644 --- a/pandas/tests/arrays/interval/test_interval.py +++ b/pandas/tests/arrays/interval/test_interval.py @@ -93,8 +93,13 @@ def test_set_na(self, left_right_dtypes): tm.assert_extension_array_equal(result, expected) -def test_repr_matches(): - idx = IntervalIndex.from_breaks([1, 2, 3]) - a = repr(idx) - b = repr(idx.values) - assert a.replace("Index", "Array") == b +def test_repr(): + # GH 25022 + arr = IntervalArray.from_tuples([(0, 1), (1, 2)]) + result = repr(arr) + expected = ( + "\n" + "[(0, 1], (1, 2]]\n" + "Length: 2, closed: right, dtype: interval[int64]" + ) + assert result == expected