diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index 5cafaa5759a5b..d7f7facde5d76 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:`54477`) 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..57783265b04b3 100644 --- a/pandas/tests/indexes/interval/test_interval_range.py +++ b/pandas/tests/indexes/interval/test_interval_range.py @@ -353,3 +353,13 @@ 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): + # 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) + + result = interval_range(0, 1, freq=0.6) + expected = IntervalIndex.from_breaks([0, 0.6]) + tm.assert_index_equal(result, expected)