diff --git a/doc/source/user_guide/advanced.rst b/doc/source/user_guide/advanced.rst index b8df21ab5a5b4..3081c6f7c6a08 100644 --- a/doc/source/user_guide/advanced.rst +++ b/doc/source/user_guide/advanced.rst @@ -1082,14 +1082,14 @@ of :ref:`frequency aliases ` with datetime-like inter pd.interval_range(start=pd.Timedelta("0 days"), periods=3, freq="9H") -Additionally, the ``closed`` parameter can be used to specify which side(s) the intervals -are closed on. Intervals are closed on the right side by default. +Additionally, the ``inclusive`` parameter can be used to specify which side(s) the intervals +are closed on. Intervals are closed on the both side by default. .. ipython:: python - pd.interval_range(start=0, end=4, closed="both") + pd.interval_range(start=0, end=4, inclusive="both") - pd.interval_range(start=0, end=4, closed="neither") + pd.interval_range(start=0, end=4, inclusive="neither") Specifying ``start``, ``end``, and ``periods`` will generate a range of evenly spaced intervals from ``start`` to ``end`` inclusively, with ``periods`` number of elements diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 56b1a6317472b..8607294244052 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -426,6 +426,7 @@ Other Deprecations - Deprecated behavior of method :meth:`DataFrame.quantile`, attribute ``numeric_only`` will default False. Including datetime/timedelta columns in the result (:issue:`7308`). - Deprecated :attr:`Timedelta.freq` and :attr:`Timedelta.is_populated` (:issue:`46430`) - Deprecated :attr:`Timedelta.delta` (:issue:`46476`) +- Deprecated the ``closed`` argument in :meth:`interval_range` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`) - .. --------------------------------------------------------------------------- diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 425e7d3f4432e..f6a20a418c32b 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -11,6 +11,7 @@ Hashable, Literal, ) +import warnings import numpy as np @@ -160,7 +161,7 @@ def _new_IntervalIndex(cls, d): A new ``IntervalIndex`` is typically constructed using :func:`interval_range`: - >>> pd.interval_range(start=0, end=5) + >>> pd.interval_range(start=0, end=5, inclusive="right") IntervalIndex([(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]], dtype='interval[int64, right]') @@ -443,7 +444,7 @@ def is_overlapping(self) -> bool: Intervals that share closed endpoints overlap: - >>> index = pd.interval_range(0, 3, closed='both') + >>> index = pd.interval_range(0, 3, inclusive='both') >>> index IntervalIndex([[0, 1], [1, 2], [2, 3]], dtype='interval[int64, both]') @@ -452,7 +453,7 @@ def is_overlapping(self) -> bool: Intervals that only have an open endpoint in common do not overlap: - >>> index = pd.interval_range(0, 3, closed='left') + >>> index = pd.interval_range(0, 3, inclusive='left') >>> index IntervalIndex([[0, 1), [1, 2), [2, 3)], dtype='interval[int64, left]') @@ -956,7 +957,8 @@ def interval_range( periods=None, freq=None, name: Hashable = None, - closed: IntervalClosedType = "right", + closed: lib.NoDefault = lib.no_default, + inclusive: IntervalClosedType | None = None, ) -> IntervalIndex: """ Return a fixed frequency IntervalIndex. @@ -979,6 +981,14 @@ def interval_range( Whether the intervals are closed on the left-side, right-side, both or neither. + .. deprecated:: 1.5.0 + Argument `closed` has been deprecated to standardize boundary inputs. + Use `inclusive` instead, to set each bound as closed or open. + inclusive : {"both", "neither", "left", "right"}, default "both" + Include boundaries; Whether to set each bound as closed or open. + + .. versionadded:: 1.5.0 + Returns ------- IntervalIndex @@ -1001,14 +1011,14 @@ def interval_range( -------- Numeric ``start`` and ``end`` is supported. - >>> pd.interval_range(start=0, end=5) + >>> pd.interval_range(start=0, end=5, inclusive="right") IntervalIndex([(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]], dtype='interval[int64, right]') Additionally, datetime-like input is also supported. >>> pd.interval_range(start=pd.Timestamp('2017-01-01'), - ... end=pd.Timestamp('2017-01-04')) + ... end=pd.Timestamp('2017-01-04'), inclusive="right") IntervalIndex([(2017-01-01, 2017-01-02], (2017-01-02, 2017-01-03], (2017-01-03, 2017-01-04]], dtype='interval[datetime64[ns], right]') @@ -1017,7 +1027,7 @@ def interval_range( endpoints of the individual intervals within the ``IntervalIndex``. For numeric ``start`` and ``end``, the frequency must also be numeric. - >>> pd.interval_range(start=0, periods=4, freq=1.5) + >>> pd.interval_range(start=0, periods=4, freq=1.5, inclusive="right") IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0]], dtype='interval[float64, right]') @@ -1025,7 +1035,7 @@ def interval_range( convertible to a DateOffset. >>> pd.interval_range(start=pd.Timestamp('2017-01-01'), - ... periods=3, freq='MS') + ... periods=3, freq='MS', inclusive="right") IntervalIndex([(2017-01-01, 2017-02-01], (2017-02-01, 2017-03-01], (2017-03-01, 2017-04-01]], dtype='interval[datetime64[ns], right]') @@ -1033,17 +1043,40 @@ def interval_range( Specify ``start``, ``end``, and ``periods``; the frequency is generated automatically (linearly spaced). - >>> pd.interval_range(start=0, end=6, periods=4) + >>> pd.interval_range(start=0, end=6, periods=4, inclusive="right") IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0]], dtype='interval[float64, right]') - The ``closed`` parameter specifies which endpoints of the individual + The ``inclusive`` parameter specifies which endpoints of the individual intervals within the ``IntervalIndex`` are closed. - >>> pd.interval_range(end=5, periods=4, closed='both') + >>> pd.interval_range(end=5, periods=4, inclusive='both') IntervalIndex([[1, 2], [2, 3], [3, 4], [4, 5]], dtype='interval[int64, both]') """ + if inclusive is not None and not isinstance(closed, lib.NoDefault): + raise ValueError( + "Deprecated argument `closed` cannot be passed " + "if argument `inclusive` is not None" + ) + elif not isinstance(closed, lib.NoDefault): + warnings.warn( + "Argument `closed` is deprecated in favor of `inclusive`.", + FutureWarning, + stacklevel=2, + ) + if closed is None: + inclusive = "both" + elif closed in ("both", "neither", "left", "right"): + inclusive = closed + else: + raise ValueError( + "Argument `closed` has to be either" + "'both', 'neither', 'left' or 'right'" + ) + elif inclusive is None: + inclusive = "both" + start = maybe_box_datetimelike(start) end = maybe_box_datetimelike(end) endpoint = start if start is not None else end @@ -1120,4 +1153,4 @@ def interval_range( else: breaks = timedelta_range(start=start, end=end, periods=periods, freq=freq) - return IntervalIndex.from_breaks(breaks, name=name, closed=closed) + return IntervalIndex.from_breaks(breaks, name=name, closed=inclusive) diff --git a/pandas/tests/frame/methods/test_round.py b/pandas/tests/frame/methods/test_round.py index dd9206940bcd6..77cadfff55e2f 100644 --- a/pandas/tests/frame/methods/test_round.py +++ b/pandas/tests/frame/methods/test_round.py @@ -210,7 +210,7 @@ def test_round_nonunique_categorical(self): def test_round_interval_category_columns(self): # GH#30063 - columns = pd.CategoricalIndex(pd.interval_range(0, 2)) + columns = pd.CategoricalIndex(pd.interval_range(0, 2, inclusive="right")) df = DataFrame([[0.66, 1.1], [0.3, 0.25]], columns=columns) result = df.round() diff --git a/pandas/tests/groupby/aggregate/test_cython.py b/pandas/tests/groupby/aggregate/test_cython.py index 96009be5d12e3..9631de7833cf4 100644 --- a/pandas/tests/groupby/aggregate/test_cython.py +++ b/pandas/tests/groupby/aggregate/test_cython.py @@ -214,7 +214,7 @@ def test_cython_agg_empty_buckets_nanops(observed): result = df.groupby(pd.cut(df["a"], grps), observed=observed)._cython_agg_general( "add", alt=None, numeric_only=True ) - intervals = pd.interval_range(0, 20, freq=5) + intervals = pd.interval_range(0, 20, freq=5, inclusive="right") expected = DataFrame( {"a": [0, 0, 36, 0]}, index=pd.CategoricalIndex(intervals, name="a", ordered=True), diff --git a/pandas/tests/indexes/interval/test_astype.py b/pandas/tests/indexes/interval/test_astype.py index c253a745ef5a2..4cdbe2bbcf12b 100644 --- a/pandas/tests/indexes/interval/test_astype.py +++ b/pandas/tests/indexes/interval/test_astype.py @@ -117,7 +117,7 @@ def test_subtype_integer(self, subtype_start, subtype_end): @pytest.mark.xfail(reason="GH#15832") def test_subtype_integer_errors(self): # int64 -> uint64 fails with negative values - index = interval_range(-10, 10) + index = interval_range(-10, 10, inclusive="right") dtype = IntervalDtype("uint64", "right") # Until we decide what the exception message _should_ be, we @@ -133,7 +133,7 @@ class TestFloatSubtype(AstypeTests): """Tests specific to IntervalIndex with float subtype""" indexes = [ - interval_range(-10.0, 10.0, closed="neither"), + interval_range(-10.0, 10.0, inclusive="neither"), IntervalIndex.from_arrays( [-1.5, np.nan, 0.0, 0.0, 1.5], [-0.5, np.nan, 1.0, 1.0, 3.0], closed="both" ), @@ -170,7 +170,7 @@ def test_subtype_integer_with_non_integer_borders(self, subtype): def test_subtype_integer_errors(self): # float64 -> uint64 fails with negative values - index = interval_range(-10.0, 10.0) + index = interval_range(-10.0, 10.0, inclusive="right") dtype = IntervalDtype("uint64", "right") msg = re.escape( "Cannot convert interval[float64, right] to interval[uint64, right]; " @@ -191,10 +191,10 @@ class TestDatetimelikeSubtype(AstypeTests): """Tests specific to IntervalIndex with datetime-like subtype""" indexes = [ - interval_range(Timestamp("2018-01-01"), periods=10, closed="neither"), + interval_range(Timestamp("2018-01-01"), periods=10, inclusive="neither"), interval_range(Timestamp("2018-01-01"), periods=10).insert(2, NaT), interval_range(Timestamp("2018-01-01", tz="US/Eastern"), periods=10), - interval_range(Timedelta("0 days"), periods=10, closed="both"), + interval_range(Timedelta("0 days"), periods=10, inclusive="both"), interval_range(Timedelta("0 days"), periods=10).insert(2, NaT), ] diff --git a/pandas/tests/indexes/interval/test_interval.py b/pandas/tests/indexes/interval/test_interval.py index 37c13c37d070b..8880cab2ce29b 100644 --- a/pandas/tests/indexes/interval/test_interval.py +++ b/pandas/tests/indexes/interval/test_interval.py @@ -167,10 +167,10 @@ def test_delete(self, closed): @pytest.mark.parametrize( "data", [ - interval_range(0, periods=10, closed="neither"), - interval_range(1.7, periods=8, freq=2.5, closed="both"), - interval_range(Timestamp("20170101"), periods=12, closed="left"), - interval_range(Timedelta("1 day"), periods=6, closed="right"), + interval_range(0, periods=10, inclusive="neither"), + interval_range(1.7, periods=8, freq=2.5, inclusive="both"), + interval_range(Timestamp("20170101"), periods=12, inclusive="left"), + interval_range(Timedelta("1 day"), periods=6, inclusive="right"), ], ) def test_insert(self, data): @@ -868,9 +868,9 @@ def test_nbytes(self): @pytest.mark.parametrize("new_closed", ["left", "right", "both", "neither"]) def test_set_closed(self, name, closed, new_closed): # GH 21670 - index = interval_range(0, 5, closed=closed, name=name) + index = interval_range(0, 5, inclusive=closed, name=name) result = index.set_closed(new_closed) - expected = interval_range(0, 5, closed=new_closed, name=name) + expected = interval_range(0, 5, inclusive=new_closed, name=name) tm.assert_index_equal(result, expected) @pytest.mark.parametrize("bad_closed", ["foo", 10, "LEFT", True, False]) diff --git a/pandas/tests/indexes/interval/test_interval_range.py b/pandas/tests/indexes/interval/test_interval_range.py index 2f28c33a3bbc6..63e7f3aa2b120 100644 --- a/pandas/tests/indexes/interval/test_interval_range.py +++ b/pandas/tests/indexes/interval/test_interval_range.py @@ -34,25 +34,25 @@ def test_constructor_numeric(self, closed, name, freq, periods): # defined from start/end/freq result = interval_range( - start=start, end=end, freq=freq, name=name, closed=closed + start=start, end=end, freq=freq, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) # defined from start/periods/freq result = interval_range( - start=start, periods=periods, freq=freq, name=name, closed=closed + start=start, periods=periods, freq=freq, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) # defined from end/periods/freq result = interval_range( - end=end, periods=periods, freq=freq, name=name, closed=closed + end=end, periods=periods, freq=freq, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) # GH 20976: linspace behavior defined from start/end/periods result = interval_range( - start=start, end=end, periods=periods, name=name, closed=closed + start=start, end=end, periods=periods, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) @@ -67,19 +67,19 @@ def test_constructor_timestamp(self, closed, name, freq, periods, tz): # defined from start/end/freq result = interval_range( - start=start, end=end, freq=freq, name=name, closed=closed + start=start, end=end, freq=freq, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) # defined from start/periods/freq result = interval_range( - start=start, periods=periods, freq=freq, name=name, closed=closed + start=start, periods=periods, freq=freq, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) # defined from end/periods/freq result = interval_range( - end=end, periods=periods, freq=freq, name=name, closed=closed + end=end, periods=periods, freq=freq, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) @@ -88,7 +88,7 @@ def test_constructor_timestamp(self, closed, name, freq, periods, tz): # matches expected only for non-anchored offsets and tz naive # (anchored/DST transitions cause unequal spacing in expected) result = interval_range( - start=start, end=end, periods=periods, name=name, closed=closed + start=start, end=end, periods=periods, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) @@ -102,25 +102,25 @@ def test_constructor_timedelta(self, closed, name, freq, periods): # defined from start/end/freq result = interval_range( - start=start, end=end, freq=freq, name=name, closed=closed + start=start, end=end, freq=freq, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) # defined from start/periods/freq result = interval_range( - start=start, periods=periods, freq=freq, name=name, closed=closed + start=start, periods=periods, freq=freq, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) # defined from end/periods/freq result = interval_range( - end=end, periods=periods, freq=freq, name=name, closed=closed + end=end, periods=periods, freq=freq, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) # GH 20976: linspace behavior defined from start/end/periods result = interval_range( - start=start, end=end, periods=periods, name=name, closed=closed + start=start, end=end, periods=periods, name=name, inclusive=closed ) tm.assert_index_equal(result, expected) @@ -163,7 +163,9 @@ def test_no_invalid_float_truncation(self, start, end, freq): breaks = [0.5, 2.0, 3.5, 5.0, 6.5] expected = IntervalIndex.from_breaks(breaks) - result = interval_range(start=start, end=end, periods=4, freq=freq) + result = interval_range( + start=start, end=end, periods=4, freq=freq, inclusive="right" + ) tm.assert_index_equal(result, expected) @pytest.mark.parametrize( @@ -184,7 +186,7 @@ def test_no_invalid_float_truncation(self, start, end, freq): def test_linspace_dst_transition(self, start, mid, end): # GH 20976: linspace behavior defined from start/end/periods # accounts for the hour gained/lost during DST transition - result = interval_range(start=start, end=end, periods=2) + result = interval_range(start=start, end=end, periods=2, inclusive="right") expected = IntervalIndex.from_breaks([start, mid, end]) tm.assert_index_equal(result, expected) @@ -353,3 +355,17 @@ 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_interval_range_error_and_warning(self): + # GH 40245 + + msg = ( + "Deprecated argument `closed` cannot " + "be passed if argument `inclusive` is not None" + ) + with pytest.raises(ValueError, match=msg): + interval_range(end=5, periods=4, closed="both", inclusive="both") + + msg = "Argument `closed` is deprecated in favor of `inclusive`" + with tm.assert_produces_warning(FutureWarning, match=msg): + interval_range(end=5, periods=4, closed="right") diff --git a/pandas/tests/indexes/interval/test_setops.py b/pandas/tests/indexes/interval/test_setops.py index 059b0b75f4190..51a1d36398aa4 100644 --- a/pandas/tests/indexes/interval/test_setops.py +++ b/pandas/tests/indexes/interval/test_setops.py @@ -194,7 +194,7 @@ def test_set_incompatible_types(self, closed, op_name, sort): tm.assert_index_equal(result, expected) # GH 19016: incompatible dtypes -> cast to object - other = interval_range(Timestamp("20180101"), periods=9, closed=closed) + other = interval_range(Timestamp("20180101"), periods=9, inclusive=closed) expected = getattr(index.astype(object), op_name)(other, sort=sort) if op_name == "difference": expected = index diff --git a/pandas/tests/indexing/test_coercion.py b/pandas/tests/indexing/test_coercion.py index 2d54a9ba370ca..4504c55698a9a 100644 --- a/pandas/tests/indexing/test_coercion.py +++ b/pandas/tests/indexing/test_coercion.py @@ -709,7 +709,7 @@ def test_fillna_datetime64tz(self, index_or_series, fill_val, fill_dtype): ], ) def test_fillna_interval(self, index_or_series, fill_val): - ii = pd.interval_range(1.0, 5.0, closed="right").insert(1, np.nan) + ii = pd.interval_range(1.0, 5.0, inclusive="right").insert(1, np.nan) assert isinstance(ii.dtype, pd.IntervalDtype) obj = index_or_series(ii) diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py index 4702b5e5c4504..3f8e4401808b7 100644 --- a/pandas/tests/indexing/test_loc.py +++ b/pandas/tests/indexing/test_loc.py @@ -1508,12 +1508,12 @@ def test_loc_getitem_interval_index(self): def test_loc_getitem_interval_index2(self): # GH#19977 - index = pd.interval_range(start=0, periods=3, closed="both") + index = pd.interval_range(start=0, periods=3, inclusive="both") df = DataFrame( [[1, 2, 3], [4, 5, 6], [7, 8, 9]], index=index, columns=["A", "B", "C"] ) - index_exp = pd.interval_range(start=0, periods=2, freq=1, closed="both") + index_exp = pd.interval_range(start=0, periods=2, freq=1, inclusive="both") expected = Series([1, 4], index=index_exp, name="A") result = df.loc[1, "A"] tm.assert_series_equal(result, expected) diff --git a/pandas/tests/io/excel/test_writers.py b/pandas/tests/io/excel/test_writers.py index e475c9a4c6145..26c283a7dcc45 100644 --- a/pandas/tests/io/excel/test_writers.py +++ b/pandas/tests/io/excel/test_writers.py @@ -282,7 +282,10 @@ def test_multiindex_interval_datetimes(self, ext): [ range(4), pd.interval_range( - start=pd.Timestamp("2020-01-01"), periods=4, freq="6M" + start=pd.Timestamp("2020-01-01"), + periods=4, + freq="6M", + inclusive="right", ), ] ) diff --git a/pandas/tests/series/indexing/test_setitem.py b/pandas/tests/series/indexing/test_setitem.py index 992f67c2affc6..1e84a05e2ae97 100644 --- a/pandas/tests/series/indexing/test_setitem.py +++ b/pandas/tests/series/indexing/test_setitem.py @@ -779,7 +779,7 @@ def test_index_putmask(self, obj, key, expected, val): pytest.param( # GH#45568 setting a valid NA value into IntervalDtype[int] should # cast to IntervalDtype[float] - Series(interval_range(1, 5)), + Series(interval_range(1, 5, inclusive="right")), Series( [Interval(1, 2), np.nan, Interval(3, 4), Interval(4, 5)], dtype="interval[float64]", @@ -1356,7 +1356,7 @@ class TestPeriodIntervalCoercion(CoercionTest): @pytest.fixture( params=[ period_range("2016-01-01", periods=3, freq="D"), - interval_range(1, 5), + interval_range(1, 5, inclusive="right"), ] ) def obj(self, request): diff --git a/pandas/tests/util/test_assert_interval_array_equal.py b/pandas/tests/util/test_assert_interval_array_equal.py index 8cc4ade3d7e95..243f357d7298c 100644 --- a/pandas/tests/util/test_assert_interval_array_equal.py +++ b/pandas/tests/util/test_assert_interval_array_equal.py @@ -9,7 +9,7 @@ [ {"start": 0, "periods": 4}, {"start": 1, "periods": 5}, - {"start": 5, "end": 10, "closed": "left"}, + {"start": 5, "end": 10, "inclusive": "left"}, ], ) def test_interval_array_equal(kwargs): @@ -19,8 +19,8 @@ def test_interval_array_equal(kwargs): def test_interval_array_equal_closed_mismatch(): kwargs = {"start": 0, "periods": 5} - arr1 = interval_range(closed="left", **kwargs).values - arr2 = interval_range(closed="right", **kwargs).values + arr1 = interval_range(inclusive="left", **kwargs).values + arr2 = interval_range(inclusive="right", **kwargs).values msg = """\ IntervalArray are different