Skip to content

Commit 8a4c2c7

Browse files
jbrockmendelclaude
andcommitted
Rework deprecation around future.infer_freq_returns_offset option
Mirror the GH#55504 inferred_freq pattern: respect the option to opt in to (or out of) the future BaseOffset behavior, only warn when inference succeeds, fix the migration advice (DatetimeIndex(ser).freq is always None now that arrays carry no freq), and add dedicated tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e9129f8 commit 8a4c2c7

4 files changed

Lines changed: 83 additions & 30 deletions

File tree

doc/source/whatsnew/v3.1.0.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Other API changes
104104

105105
Deprecations
106106
~~~~~~~~~~~~
107-
- Deprecated :attr:`Series.dt.freq` returning a string. In a future version, this will return a :class:`DateOffset` object (or ``None``) to match :attr:`DatetimeIndex.freq` (:issue:`64701`)
107+
- Deprecated :attr:`Series.dt.freq` returning a string; in a future version this will return a :class:`BaseOffset` instead. Use ``pd.set_option('future.infer_freq_returns_offset', True)`` to opt in to the future behavior (:issue:`64701`)
108108
- Deprecated :attr:`Timestamp.dayofweek`, :attr:`Timestamp.dayofyear`, :attr:`Timestamp.daysinmonth` in favor of :attr:`Timestamp.day_of_week`, :attr:`Timestamp.day_of_year`, :attr:`Timestamp.days_in_month`, respectively. The same deprecation applies to the corresponding attributes on :class:`Period`, :class:`DatetimeIndex`, :class:`PeriodIndex`, and :attr:`Series.dt` (:issue:`46768`)
109109
- Deprecated :func:`infer_freq`, :attr:`DatetimeIndex.inferred_freq`, and :attr:`TimedeltaIndex.inferred_freq` returning a string; in a future version these will return a :class:`BaseOffset` instead. Use ``pd.set_option('future.infer_freq_returns_offset', True)`` to opt in to the future behavior (:issue:`55504`)
110110
- Deprecated :func:`set_eng_float_format`. Use ``pd.set_option("display.precision", N)`` to control decimal precision, or pass a custom callable to ``pd.set_option("display.float_format", func)`` (:issue:`64460`)

pandas/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,10 @@ def pytest_collection_modifyitems(items, config) -> None:
214214
("read_parquet", "Passing a BlockManager to DataFrame is deprecated"),
215215
("Timestamp.utcfromtimestamp", "Timestamp.utcfromtimestamp is deprecated"),
216216
("BaseOffset.name.__get__", "The 'name' property is deprecated"),
217-
("DatetimeProperties.freq", "The behavior of Series.dt.freq is deprecated"),
217+
(
218+
"DatetimeProperties.freq",
219+
"A future version of pandas will return a BaseOffset",
220+
),
218221
]
219222

220223
if is_doctest:

pandas/core/indexes/accessors.py

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
import numpy as np
1515

16+
from pandas._config import using_infer_freq_offset
17+
1618
from pandas._libs import lib
19+
from pandas._libs.tslibs import to_offset
1720
from pandas.errors import Pandas4Warning
1821
from pandas.util._exceptions import find_stack_level
1922

@@ -377,12 +380,13 @@ def freq(self):
377380
"""
378381
Return a string representing a frequency generated by infer_freq.
379382
383+
Returns None if it can't autodetect the frequency.
384+
380385
.. deprecated:: 3.1.0
381-
The behavior of :attr:`Series.dt.freq` is deprecated. In a future
382-
version, this will return the frequency as a :class:`DateOffset`
383-
object (or ``None``) instead of as a string. To get the future
384-
behavior, use ``Series.dt.freq`` on a Series backed by a
385-
:class:`DatetimeIndex` with a set frequency.
386+
A future version of pandas will return a :class:`BaseOffset`
387+
object (or ``None``) instead of a string. Use
388+
``pd.set_option('future.infer_freq_returns_offset', True)`` to
389+
opt in to the future behavior.
386390
387391
See Also
388392
--------
@@ -401,16 +405,23 @@ def freq(self):
401405
>>> ser.dt.freq
402406
'2YS-JAN'
403407
"""
404-
warnings.warn(
405-
"The behavior of Series.dt.freq is deprecated. "
406-
"In a future version, this will return the frequency as a "
407-
"DateOffset object (or None) instead of as a string. "
408-
"To silence this warning, use "
409-
"DatetimeIndex(ser).freq instead of ser.dt.freq",
410-
Pandas4Warning,
411-
stacklevel=find_stack_level(),
412-
)
413-
return self._get_values()._inferred_freq_str
408+
result = self._get_values()._inferred_freq_str
409+
if result is not None:
410+
opt = using_infer_freq_offset()
411+
if opt is True:
412+
return to_offset(result)
413+
if opt is None:
414+
warnings.warn(
415+
"A future version of pandas will return a BaseOffset "
416+
"object instead of a string from Series.dt.freq. "
417+
"Use pd.set_option("
418+
"'future.infer_freq_returns_offset', True) "
419+
"to get the future behavior, or set to False to keep the "
420+
"old behavior and silence this warning.",
421+
Pandas4Warning,
422+
stacklevel=find_stack_level(),
423+
)
424+
return result
414425

415426
def isocalendar(self) -> DataFrame:
416427
"""
@@ -570,16 +581,23 @@ def components(self) -> DataFrame:
570581

571582
@property
572583
def freq(self):
573-
warnings.warn(
574-
"The behavior of Series.dt.freq is deprecated. "
575-
"In a future version, this will return the frequency as a "
576-
"DateOffset object (or None) instead of as a string. "
577-
"To silence this warning, use "
578-
"TimedeltaIndex(ser).freq instead of ser.dt.freq",
579-
Pandas4Warning,
580-
stacklevel=find_stack_level(),
581-
)
582-
return self._get_values()._inferred_freq_str
584+
result = self._get_values()._inferred_freq_str
585+
if result is not None:
586+
opt = using_infer_freq_offset()
587+
if opt is True:
588+
return to_offset(result)
589+
if opt is None:
590+
warnings.warn(
591+
"A future version of pandas will return a BaseOffset "
592+
"object instead of a string from Series.dt.freq. "
593+
"Use pd.set_option("
594+
"'future.infer_freq_returns_offset', True) "
595+
"to get the future behavior, or set to False to keep the "
596+
"old behavior and silence this warning.",
597+
Pandas4Warning,
598+
stacklevel=find_stack_level(),
599+
)
600+
return result
583601

584602

585603
@delegate_names(

pandas/tests/series/accessors/test_dt_accessor.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def test_dt_namespace_accessor_datetime64(self, freq):
131131

132132
tz_result = result.dt.tz
133133
assert str(tz_result) == "US/Eastern"
134-
msg = "The behavior of Series.dt.freq is deprecated"
134+
msg = "A future version of pandas will return a BaseOffset"
135135
with tm.assert_produces_warning(Pandas4Warning, match=msg):
136136
freq_result = ser.dt.freq
137137
assert freq_result == DatetimeIndex(ser.values, freq="infer").freq
@@ -170,7 +170,7 @@ def test_dt_namespace_accessor_datetime64tz(self):
170170

171171
tz_result = result.dt.tz
172172
assert str(tz_result) == "CET"
173-
msg = "The behavior of Series.dt.freq is deprecated"
173+
msg = "A future version of pandas will return a BaseOffset"
174174
with tm.assert_produces_warning(Pandas4Warning, match=msg):
175175
freq_result = ser.dt.freq
176176
assert freq_result == DatetimeIndex(ser.values, freq="infer").freq
@@ -213,7 +213,7 @@ def test_dt_namespace_accessor_timedelta(self):
213213
assert isinstance(result, Series)
214214
assert result.dtype == "float64"
215215

216-
msg = "The behavior of Series.dt.freq is deprecated"
216+
msg = "A future version of pandas will return a BaseOffset"
217217
with tm.assert_produces_warning(Pandas4Warning, match=msg):
218218
freq_result = ser.dt.freq
219219
assert freq_result == TimedeltaIndex(ser.values, freq="infer").freq
@@ -885,3 +885,35 @@ def test_day_attribute_non_nano_beyond_int32():
885885
result = ser.dt.days
886886
expected = Series([1579371003, 1559453522, 2839645203, 2586, 27, 42066, 0])
887887
tm.assert_series_equal(result, expected)
888+
889+
890+
@pytest.mark.parametrize(
891+
"ser",
892+
[
893+
Series(date_range("2020-01-01", periods=3)),
894+
Series(timedelta_range("1 day", periods=3)),
895+
],
896+
)
897+
def test_dt_freq_deprecated(ser):
898+
# GH#64701
899+
msg = "A future version of pandas will return a BaseOffset"
900+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
901+
result = ser.dt.freq
902+
assert result == "D"
903+
904+
with pd.option_context("future.infer_freq_returns_offset", True):
905+
with tm.assert_produces_warning(None):
906+
result = ser.dt.freq
907+
assert result == pd.offsets.Day()
908+
909+
with pd.option_context("future.infer_freq_returns_offset", False):
910+
with tm.assert_produces_warning(None):
911+
result = ser.dt.freq
912+
assert result == "D"
913+
914+
915+
def test_dt_freq_no_warning_when_unable_to_infer():
916+
# GH#64701 - no behavior change when the result is None, so no warning
917+
ser = Series(pd.to_datetime(["2020-01-01", "2020-03-07", "2020-08-15"]))
918+
with tm.assert_produces_warning(None):
919+
assert ser.dt.freq is None

0 commit comments

Comments
 (0)