Skip to content

DEPR: fractional periods in date_range, timedelta_range, period_range… #56036

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v2.2.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ Other Deprecations
- Deprecated :meth:`.DataFrameGroupBy.fillna` and :meth:`.SeriesGroupBy.fillna`; use :meth:`.DataFrameGroupBy.ffill`, :meth:`.DataFrameGroupBy.bfill` for forward and backward filling or :meth:`.DataFrame.fillna` to fill with a single value (or the Series equivalents) (:issue:`55718`)
- Deprecated :meth:`Index.format`, use ``index.astype(str)`` or ``index.map(formatter)`` instead (:issue:`55413`)
- Deprecated ``year``, ``month``, ``quarter``, ``day``, ``hour``, ``minute``, and ``second`` keywords in the :class:`PeriodIndex` constructor, use :meth:`PeriodIndex.from_fields` instead (:issue:`55960`)
- Deprecated allowing fractional ``periods`` argument in :func:`date_range`, :func:`timedelta_range`, :func:`period_range`, and :func:`interval_range` (:issue:`56036`)
Copy link
Member

@mroeschke mroeschke Nov 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we specify float instead of fractional since e.g periods=2.0 would be disallowed as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only deprecated allowing non-round floats. id be OK with deprecating round ones too

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah let's just deprecate all floats and require periods to be integer in the future

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated+green

- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_clipboard`. (:issue:`54229`)
- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_csv` except ``path_or_buf``. (:issue:`54229`)
- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_dict`. (:issue:`54229`)
Expand Down
9 changes: 9 additions & 0 deletions pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -2454,6 +2454,15 @@ def validate_periods(periods: int | float | None) -> int | None:
"""
if periods is not None:
if lib.is_float(periods):
if not periods.is_integer():
# GH#56036
warnings.warn(
"Non-integer 'periods' in pd.date_range, pd.timedelta_range, "
"pd.period_range, and pd.interval_range are deprecated and "
"will raise in a future version.",
FutureWarning,
stacklevel=find_stack_level(),
)
periods = int(periods)
elif not lib.is_integer(periods):
raise TypeError(f"periods must be a number, got {periods}")
Expand Down
7 changes: 2 additions & 5 deletions pandas/core/indexes/interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
)
from pandas.core.dtypes.common import (
ensure_platform_int,
is_float,
is_float_dtype,
is_integer,
is_integer_dtype,
Expand All @@ -59,6 +58,7 @@
from pandas.core.dtypes.missing import is_valid_na_for_dtype

from pandas.core.algorithms import unique
from pandas.core.arrays.datetimelike import validate_periods
from pandas.core.arrays.interval import (
IntervalArray,
_interval_shared_docs,
Expand Down Expand Up @@ -1075,10 +1075,7 @@ def interval_range(
if not _is_valid_endpoint(end):
raise ValueError(f"end must be numeric or datetime-like, got {end}")

if is_float(periods):
periods = int(periods)
elif not is_integer(periods) and periods is not None:
raise TypeError(f"periods must be a number, got {periods}")
periods = validate_periods(periods)

if freq is not None and not is_number(freq):
try:
Expand Down
7 changes: 4 additions & 3 deletions pandas/tests/indexes/datetimes/test_date_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,10 @@ def test_date_range_invalid_periods(self):
with pytest.raises(TypeError, match=msg):
date_range(start="1/1/2000", periods="foo", freq="D")

def test_date_range_float_periods(self):
# TODO: reconsider allowing this?
rng = date_range("1/1/2000", periods=10.5)
def test_date_range_fractional_period(self):
msg = "Non-integer 'periods' in pd.date_range, pd.timedelta_range"
with tm.assert_produces_warning(FutureWarning, match=msg):
rng = date_range("1/1/2000", periods=10.5)
exp = date_range("1/1/2000", periods=10)
tm.assert_index_equal(rng, exp)

Expand Down
7 changes: 5 additions & 2 deletions pandas/tests/indexes/interval/test_interval_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,15 @@ def test_float_subtype(self, start, end, freq):
expected = "int64" if is_integer(start + end) else "float64"
assert result == expected

def test_constructor_coverage(self):
def test_interval_range_fractional_period(self):
# float value for periods
expected = interval_range(start=0, periods=10)
result = interval_range(start=0, periods=10.5)
msg = "Non-integer 'periods' in pd.date_range, .* pd.interval_range"
with tm.assert_produces_warning(FutureWarning, match=msg):
result = interval_range(start=0, periods=10.5)
tm.assert_index_equal(result, expected)

def test_constructor_coverage(self):
# equivalent timestamp-like start/end
start, end = Timestamp("2017-01-01"), Timestamp("2017-01-15")
expected = interval_range(start=start, end=end)
Expand Down
6 changes: 4 additions & 2 deletions pandas/tests/indexes/period/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,10 @@ def test_constructor_invalid_quarters(self):
year=range(2000, 2004), quarter=list(range(4)), freq="Q-DEC"
)

def test_constructor_corner(self):
result = period_range("2007-01", periods=10.5, freq="M")
def test_period_range_fractional_period(self):
msg = "Non-integer 'periods' in pd.date_range, pd.timedelta_range"
with tm.assert_produces_warning(FutureWarning, match=msg):
result = period_range("2007-01", periods=10.5, freq="M")
exp = period_range("2007-01", periods=10, freq="M")
tm.assert_index_equal(result, exp)

Expand Down
7 changes: 5 additions & 2 deletions pandas/tests/indexes/timedeltas/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,14 @@ def test_constructor_iso(self):
result = to_timedelta(durations)
tm.assert_index_equal(result, expected)

def test_constructor_coverage(self):
rng = timedelta_range("1 days", periods=10.5)
def test_timedelta_range_fractional_period(self):
msg = "Non-integer 'periods' in pd.date_range, pd.timedelta_range"
with tm.assert_produces_warning(FutureWarning, match=msg):
rng = timedelta_range("1 days", periods=10.5)
exp = timedelta_range("1 days", periods=10)
tm.assert_index_equal(rng, exp)

def test_constructor_coverage(self):
msg = "periods must be a number, got foo"
with pytest.raises(TypeError, match=msg):
timedelta_range(start="1 days", periods="foo", freq="D")
Expand Down