Skip to content

Commit dae5c59

Browse files
authored
DEPR: Timestamp.freq (#41586)
1 parent a2c65df commit dae5c59

27 files changed

+559
-140
lines changed

doc/source/reference/offset_frequency.rst

+212
Large diffs are not rendered by default.

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,7 @@ Deprecations
691691
- Deprecated special treatment of lists with first element a Categorical in the :class:`DataFrame` constructor; pass as ``pd.DataFrame({col: categorical, ...})`` instead (:issue:`38845`)
692692
- Deprecated behavior of :class:`DataFrame` constructor when a ``dtype`` is passed and the data cannot be cast to that dtype. In a future version, this will raise instead of being silently ignored (:issue:`24435`)
693693
- Deprecated passing arguments as positional (except for ``"method"``) in :meth:`DataFrame.interpolate` and :meth:`Series.interpolate` (:issue:`41485`)
694+
- Deprecated the :attr:`Timestamp.freq` attribute. For the properties that use it (``is_month_start``, ``is_month_end``, ``is_quarter_start``, ``is_quarter_end``, ``is_year_start``, ``is_year_end``), when you have a ``freq``, use e.g. ``freq.is_month_start(ts)`` (:issue:`15146`)
694695
- Deprecated passing arguments as positional in :meth:`DataFrame.ffill`, :meth:`Series.ffill`, :meth:`DataFrame.bfill`, and :meth:`Series.bfill` (:issue:`41485`)
695696
- Deprecated passing arguments as positional in :meth:`DataFrame.sort_values` (other than ``"by"``) and :meth:`Series.sort_values` (:issue:`41485`)
696697
- Deprecated passing arguments as positional in :meth:`DataFrame.dropna` and :meth:`Series.dropna` (:issue:`41485`)

pandas/_libs/tslibs/offsets.pyx

+20
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,26 @@ cdef class BaseOffset:
716716
# if there were a canonical docstring for what is_anchored means.
717717
return self.n == 1
718718

719+
# ------------------------------------------------------------------
720+
721+
def is_month_start(self, _Timestamp ts):
722+
return ts._get_start_end_field("is_month_start", self)
723+
724+
def is_month_end(self, _Timestamp ts):
725+
return ts._get_start_end_field("is_month_end", self)
726+
727+
def is_quarter_start(self, _Timestamp ts):
728+
return ts._get_start_end_field("is_quarter_start", self)
729+
730+
def is_quarter_end(self, _Timestamp ts):
731+
return ts._get_start_end_field("is_quarter_end", self)
732+
733+
def is_year_start(self, _Timestamp ts):
734+
return ts._get_start_end_field("is_year_start", self)
735+
736+
def is_year_end(self, _Timestamp ts):
737+
return ts._get_start_end_field("is_year_end", self)
738+
719739

720740
cdef class SingleConstructorOffset(BaseOffset):
721741
@classmethod

pandas/_libs/tslibs/timestamps.pxd

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ cdef object create_timestamp_from_ts(int64_t value,
1616
cdef class _Timestamp(ABCTimestamp):
1717
cdef readonly:
1818
int64_t value, nanosecond
19-
object freq
19+
object _freq
2020

21-
cdef bint _get_start_end_field(self, str field)
21+
cdef bint _get_start_end_field(self, str field, freq)
2222
cdef _get_date_name_field(self, str field, object locale)
2323
cdef int64_t _maybe_convert_value_to_local(self)
2424
cdef bint _can_compare(self, datetime other)
2525
cpdef to_datetime64(self)
2626
cpdef datetime to_pydatetime(_Timestamp self, bint warn=*)
2727
cdef bint _compare_outside_nanorange(_Timestamp self, datetime other,
2828
int op) except -1
29+
cpdef void _set_freq(self, freq)
30+
cdef _warn_on_field_deprecation(_Timestamp self, freq, str field)

pandas/_libs/tslibs/timestamps.pyi

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ from typing import (
1818
import numpy as np
1919

2020
from pandas._libs.tslibs import (
21+
BaseOffset,
2122
NaT,
2223
NaTType,
2324
Period,
@@ -57,6 +58,8 @@ class Timestamp(datetime):
5758
fold: int | None= ...,
5859
) -> _S | NaTType: ...
5960

61+
def _set_freq(self, freq: BaseOffset | None) -> None: ...
62+
6063
@property
6164
def year(self) -> int: ...
6265
@property

pandas/_libs/tslibs/timestamps.pyx

+108-30
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ cdef inline object create_timestamp_from_ts(int64_t value,
123123
dts.day, dts.hour, dts.min,
124124
dts.sec, dts.us, tz, fold=fold)
125125
ts_base.value = value
126-
ts_base.freq = freq
126+
ts_base._freq = freq
127127
ts_base.nanosecond = dts.ps // 1000
128128

129129
return ts_base
@@ -155,6 +155,21 @@ cdef class _Timestamp(ABCTimestamp):
155155
dayofweek = _Timestamp.day_of_week
156156
dayofyear = _Timestamp.day_of_year
157157

158+
cpdef void _set_freq(self, freq):
159+
# set the ._freq attribute without going through the constructor,
160+
# which would issue a warning
161+
# Caller is responsible for validation
162+
self._freq = freq
163+
164+
@property
165+
def freq(self):
166+
warnings.warn(
167+
"Timestamp.freq is deprecated and will be removed in a future version",
168+
FutureWarning,
169+
stacklevel=1,
170+
)
171+
return self._freq
172+
158173
def __hash__(_Timestamp self):
159174
if self.nanosecond:
160175
return hash(self.value)
@@ -263,7 +278,9 @@ cdef class _Timestamp(ABCTimestamp):
263278

264279
if is_any_td_scalar(other):
265280
nanos = delta_to_nanoseconds(other)
266-
result = type(self)(self.value + nanos, tz=self.tzinfo, freq=self.freq)
281+
result = type(self)(self.value + nanos, tz=self.tzinfo)
282+
if result is not NaT:
283+
result._set_freq(self._freq) # avoid warning in constructor
267284
return result
268285

269286
elif is_integer_object(other):
@@ -361,18 +378,17 @@ cdef class _Timestamp(ABCTimestamp):
361378
val = self.value
362379
return val
363380

364-
cdef bint _get_start_end_field(self, str field):
381+
cdef bint _get_start_end_field(self, str field, freq):
365382
cdef:
366383
int64_t val
367384
dict kwds
368385
ndarray[uint8_t, cast=True] out
369386
int month_kw
370387

371-
freq = self.freq
372388
if freq:
373389
kwds = freq.kwds
374390
month_kw = kwds.get('startingMonth', kwds.get('month', 12))
375-
freqstr = self.freqstr
391+
freqstr = self._freqstr
376392
else:
377393
month_kw = 12
378394
freqstr = None
@@ -382,6 +398,31 @@ cdef class _Timestamp(ABCTimestamp):
382398
field, freqstr, month_kw)
383399
return out[0]
384400

401+
cdef _warn_on_field_deprecation(self, freq, str field):
402+
"""
403+
Warn if the removal of .freq change the value of start/end properties.
404+
"""
405+
cdef:
406+
bint needs = False
407+
408+
if freq is not None:
409+
kwds = freq.kwds
410+
month_kw = kwds.get("startingMonth", kwds.get("month", 12))
411+
freqstr = self._freqstr
412+
if month_kw != 12:
413+
needs = True
414+
if freqstr.startswith("B"):
415+
needs = True
416+
417+
if needs:
418+
warnings.warn(
419+
"Timestamp.freq is deprecated and will be removed in a future "
420+
"version. When you have a freq, use "
421+
f"freq.{field}(timestamp) instead",
422+
FutureWarning,
423+
stacklevel=1,
424+
)
425+
385426
@property
386427
def is_month_start(self) -> bool:
387428
"""
@@ -397,10 +438,11 @@ cdef class _Timestamp(ABCTimestamp):
397438
>>> ts.is_month_start
398439
True
399440
"""
400-
if self.freq is None:
441+
if self._freq is None:
401442
# fast-path for non-business frequencies
402443
return self.day == 1
403-
return self._get_start_end_field("is_month_start")
444+
self._warn_on_field_deprecation(self._freq, "is_month_start")
445+
return self._get_start_end_field("is_month_start", self._freq)
404446

405447
@property
406448
def is_month_end(self) -> bool:
@@ -417,10 +459,11 @@ cdef class _Timestamp(ABCTimestamp):
417459
>>> ts.is_month_end
418460
True
419461
"""
420-
if self.freq is None:
462+
if self._freq is None:
421463
# fast-path for non-business frequencies
422464
return self.day == self.days_in_month
423-
return self._get_start_end_field("is_month_end")
465+
self._warn_on_field_deprecation(self._freq, "is_month_end")
466+
return self._get_start_end_field("is_month_end", self._freq)
424467

425468
@property
426469
def is_quarter_start(self) -> bool:
@@ -437,10 +480,11 @@ cdef class _Timestamp(ABCTimestamp):
437480
>>> ts.is_quarter_start
438481
True
439482
"""
440-
if self.freq is None:
483+
if self._freq is None:
441484
# fast-path for non-business frequencies
442485
return self.day == 1 and self.month % 3 == 1
443-
return self._get_start_end_field("is_quarter_start")
486+
self._warn_on_field_deprecation(self._freq, "is_quarter_start")
487+
return self._get_start_end_field("is_quarter_start", self._freq)
444488

445489
@property
446490
def is_quarter_end(self) -> bool:
@@ -457,10 +501,11 @@ cdef class _Timestamp(ABCTimestamp):
457501
>>> ts.is_quarter_end
458502
True
459503
"""
460-
if self.freq is None:
504+
if self._freq is None:
461505
# fast-path for non-business frequencies
462506
return (self.month % 3) == 0 and self.day == self.days_in_month
463-
return self._get_start_end_field("is_quarter_end")
507+
self._warn_on_field_deprecation(self._freq, "is_quarter_end")
508+
return self._get_start_end_field("is_quarter_end", self._freq)
464509

465510
@property
466511
def is_year_start(self) -> bool:
@@ -477,10 +522,11 @@ cdef class _Timestamp(ABCTimestamp):
477522
>>> ts.is_year_start
478523
True
479524
"""
480-
if self.freq is None:
525+
if self._freq is None:
481526
# fast-path for non-business frequencies
482527
return self.day == self.month == 1
483-
return self._get_start_end_field("is_year_start")
528+
self._warn_on_field_deprecation(self._freq, "is_year_start")
529+
return self._get_start_end_field("is_year_start", self._freq)
484530

485531
@property
486532
def is_year_end(self) -> bool:
@@ -497,10 +543,11 @@ cdef class _Timestamp(ABCTimestamp):
497543
>>> ts.is_year_end
498544
True
499545
"""
500-
if self.freq is None:
546+
if self._freq is None:
501547
# fast-path for non-business frequencies
502548
return self.month == 12 and self.day == 31
503-
return self._get_start_end_field("is_year_end")
549+
self._warn_on_field_deprecation(self._freq, "is_year_end")
550+
return self._get_start_end_field("is_year_end", self._freq)
504551

505552
cdef _get_date_name_field(self, str field, object locale):
506553
cdef:
@@ -673,11 +720,11 @@ cdef class _Timestamp(ABCTimestamp):
673720

674721
def __setstate__(self, state):
675722
self.value = state[0]
676-
self.freq = state[1]
723+
self._freq = state[1]
677724
self.tzinfo = state[2]
678725

679726
def __reduce__(self):
680-
object_state = self.value, self.freq, self.tzinfo
727+
object_state = self.value, self._freq, self.tzinfo
681728
return (Timestamp, object_state)
682729

683730
# -----------------------------------------------------------------
@@ -719,7 +766,7 @@ cdef class _Timestamp(ABCTimestamp):
719766
pass
720767

721768
tz = f", tz='{zone}'" if zone is not None else ""
722-
freq = "" if self.freq is None else f", freq='{self.freqstr}'"
769+
freq = "" if self._freq is None else f", freq='{self._freqstr}'"
723770

724771
return f"Timestamp('{stamp}'{tz}{freq})"
725772

@@ -877,7 +924,13 @@ cdef class _Timestamp(ABCTimestamp):
877924
)
878925

879926
if freq is None:
880-
freq = self.freq
927+
freq = self._freq
928+
warnings.warn(
929+
"In a future version, calling 'Timestamp.to_period()' without "
930+
"passing a 'freq' will raise an exception.",
931+
FutureWarning,
932+
stacklevel=2,
933+
)
881934

882935
return Period(self, freq=freq)
883936

@@ -1147,7 +1200,7 @@ class Timestamp(_Timestamp):
11471200
nanosecond=None,
11481201
tzinfo_type tzinfo=None,
11491202
*,
1150-
fold=None
1203+
fold=None,
11511204
):
11521205
# The parameter list folds together legacy parameter names (the first
11531206
# four) and positional and keyword parameter names from pydatetime.
@@ -1276,9 +1329,16 @@ class Timestamp(_Timestamp):
12761329

12771330
if freq is None:
12781331
# GH 22311: Try to extract the frequency of a given Timestamp input
1279-
freq = getattr(ts_input, 'freq', None)
1280-
elif not is_offset_object(freq):
1281-
freq = to_offset(freq)
1332+
freq = getattr(ts_input, '_freq', None)
1333+
else:
1334+
warnings.warn(
1335+
"The 'freq' argument in Timestamp is deprecated and will be "
1336+
"removed in a future version.",
1337+
FutureWarning,
1338+
stacklevel=1,
1339+
)
1340+
if not is_offset_object(freq):
1341+
freq = to_offset(freq)
12821342

12831343
return create_timestamp_from_ts(ts.value, ts.dts, ts.tzinfo, freq, ts.fold)
12841344

@@ -1551,12 +1611,21 @@ timedelta}, default 'raise'
15511611
"Use tz_localize() or tz_convert() as appropriate"
15521612
)
15531613

1614+
@property
1615+
def _freqstr(self):
1616+
return getattr(self._freq, "freqstr", self._freq)
1617+
15541618
@property
15551619
def freqstr(self):
15561620
"""
15571621
Return the total number of days in the month.
15581622
"""
1559-
return getattr(self.freq, 'freqstr', self.freq)
1623+
warnings.warn(
1624+
"Timestamp.freqstr is deprecated and will be removed in a future version.",
1625+
FutureWarning,
1626+
stacklevel=1,
1627+
)
1628+
return self._freqstr
15601629

15611630
def tz_localize(self, tz, ambiguous='raise', nonexistent='raise'):
15621631
"""
@@ -1647,12 +1716,18 @@ default 'raise'
16471716
value = tz_localize_to_utc_single(self.value, tz,
16481717
ambiguous=ambiguous,
16491718
nonexistent=nonexistent)
1650-
return Timestamp(value, tz=tz, freq=self.freq)
1719+
out = Timestamp(value, tz=tz)
1720+
if out is not NaT:
1721+
out._set_freq(self._freq) # avoid warning in constructor
1722+
return out
16511723
else:
16521724
if tz is None:
16531725
# reset tz
16541726
value = tz_convert_from_utc_single(self.value, self.tz)
1655-
return Timestamp(value, tz=tz, freq=self.freq)
1727+
out = Timestamp(value, tz=tz)
1728+
if out is not NaT:
1729+
out._set_freq(self._freq) # avoid warning in constructor
1730+
return out
16561731
else:
16571732
raise TypeError(
16581733
"Cannot localize tz-aware Timestamp, use tz_convert for conversions"
@@ -1707,7 +1782,10 @@ default 'raise'
17071782
)
17081783
else:
17091784
# Same UTC timestamp, different time zone
1710-
return Timestamp(self.value, tz=tz, freq=self.freq)
1785+
out = Timestamp(self.value, tz=tz)
1786+
if out is not NaT:
1787+
out._set_freq(self._freq) # avoid warning in constructor
1788+
return out
17111789

17121790
astimezone = tz_convert
17131791

@@ -1840,7 +1918,7 @@ default 'raise'
18401918
if value != NPY_NAT:
18411919
check_dts_bounds(&dts)
18421920

1843-
return create_timestamp_from_ts(value, dts, tzobj, self.freq, fold)
1921+
return create_timestamp_from_ts(value, dts, tzobj, self._freq, fold)
18441922

18451923
def to_julian_date(self) -> np.float64:
18461924
"""

pandas/core/arrays/datetimelike.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1189,7 +1189,11 @@ def _addsub_object_array(self, other: np.ndarray, op):
11891189
# Caller is responsible for broadcasting if necessary
11901190
assert self.shape == other.shape, (self.shape, other.shape)
11911191

1192-
res_values = op(self.astype("O"), np.asarray(other))
1192+
with warnings.catch_warnings():
1193+
# filter out warnings about Timestamp.freq
1194+
warnings.filterwarnings("ignore", category=FutureWarning)
1195+
res_values = op(self.astype("O"), np.asarray(other))
1196+
11931197
result = pd_array(res_values.ravel())
11941198
# error: Item "ExtensionArray" of "Union[Any, ExtensionArray]" has no attribute
11951199
# "reshape"

0 commit comments

Comments
 (0)