From 4d456a11be7355b3610eaa5372f09d8a6aa20623 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 9 Aug 2023 15:00:10 -0700 Subject: [PATCH 1/2] BUG: interval_range with float step --- doc/source/whatsnew/v2.1.0.rst | 1 + pandas/core/indexes/interval.py | 24 +++++++++---------- .../indexes/interval/test_interval_range.py | 9 +++++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index 5cafaa5759a5b..88c2dc44be09b 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -695,6 +695,7 @@ Interval ^^^^^^^^ - :meth:`pd.IntervalIndex.get_indexer` and :meth:`pd.IntervalIndex.get_indexer_nonunique` raising if ``target`` is read-only array (:issue:`53703`) - Bug in :class:`IntervalDtype` where the object could be kept alive when deleted (:issue:`54184`) +- Bug in :func:`interval_range` where a float ``step`` would produce incorrect intervals from floating point artifacts (:issue:`?`) Indexing ^^^^^^^^ diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 1f6c275934070..f915c08bb8294 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -1121,19 +1121,19 @@ def interval_range( breaks: np.ndarray | TimedeltaIndex | DatetimeIndex if is_number(endpoint): - # force consistency between start/end/freq (lower end if freq skips it) if com.all_not_none(start, end, freq): - end -= (end - start) % freq - - # compute the period/start/end if unspecified (at most one) - if periods is None: - periods = int((end - start) // freq) + 1 - elif start is None: - start = end - (periods - 1) * freq - elif end is None: - end = start + (periods - 1) * freq - - breaks = np.linspace(start, end, periods) + # 0.1 ensures we capture end + breaks = np.arange(start, end + (freq * 0.1), freq) + else: + # compute the period/start/end if unspecified (at most one) + if periods is None: + periods = int((end - start) // freq) + 1 + elif start is None: + start = end - (periods - 1) * freq + elif end is None: + end = start + (periods - 1) * freq + + breaks = np.linspace(start, end, periods) if all(is_integer(x) for x in com.not_none(start, end, freq)): # np.linspace always produces float output diff --git a/pandas/tests/indexes/interval/test_interval_range.py b/pandas/tests/indexes/interval/test_interval_range.py index 18b5af00c8d5d..11afb5dcb910d 100644 --- a/pandas/tests/indexes/interval/test_interval_range.py +++ b/pandas/tests/indexes/interval/test_interval_range.py @@ -353,3 +353,12 @@ def test_errors(self): msg = "Start and end cannot both be tz-aware with different timezones" with pytest.raises(TypeError, match=msg): interval_range(start=start, end=end) + + def test_float_freq(self): + result = interval_range(0, 1, freq=0.1) + expected = IntervalIndex.from_breaks([0 + 0.1 * n for n in range(11)]) + tm.assert_index_equal(result, expected) + + result = interval_range(0, 1, freq=0.6) + expected = IntervalIndex.from_breaks([0, 0.6]) + tm.assert_index_equal(result, expected) From fa37da2b46ccea3bfcf7176c7370b1bc3a654f07 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 9 Aug 2023 15:01:35 -0700 Subject: [PATCH 2/2] gh number --- doc/source/whatsnew/v2.1.0.rst | 2 +- pandas/tests/indexes/interval/test_interval_range.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index 88c2dc44be09b..d7f7facde5d76 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -695,7 +695,7 @@ Interval ^^^^^^^^ - :meth:`pd.IntervalIndex.get_indexer` and :meth:`pd.IntervalIndex.get_indexer_nonunique` raising if ``target`` is read-only array (:issue:`53703`) - Bug in :class:`IntervalDtype` where the object could be kept alive when deleted (:issue:`54184`) -- Bug in :func:`interval_range` where a float ``step`` would produce incorrect intervals from floating point artifacts (:issue:`?`) +- Bug in :func:`interval_range` where a float ``step`` would produce incorrect intervals from floating point artifacts (:issue:`54477`) Indexing ^^^^^^^^ diff --git a/pandas/tests/indexes/interval/test_interval_range.py b/pandas/tests/indexes/interval/test_interval_range.py index 11afb5dcb910d..57783265b04b3 100644 --- a/pandas/tests/indexes/interval/test_interval_range.py +++ b/pandas/tests/indexes/interval/test_interval_range.py @@ -355,6 +355,7 @@ def test_errors(self): interval_range(start=start, end=end) def test_float_freq(self): + # GH 54477 result = interval_range(0, 1, freq=0.1) expected = IntervalIndex.from_breaks([0 + 0.1 * n for n in range(11)]) tm.assert_index_equal(result, expected)