Skip to content

Commit 1725c94

Browse files
committed
API: Implement set_freq for DTI/TDI, deprecate freq setter
1 parent 28dbae9 commit 1725c94

File tree

13 files changed

+184
-73
lines changed

13 files changed

+184
-73
lines changed

doc/source/whatsnew/v0.23.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ Other Enhancements
524524
- Added new writer for exporting Stata dta files in version 117, ``StataWriter117``. This format supports exporting strings with lengths up to 2,000,000 characters (:issue:`16450`)
525525
- :func:`to_hdf` and :func:`read_hdf` now accept an ``errors`` keyword argument to control encoding error handling (:issue:`20835`)
526526
- :func:`date_range` now returns a linearly spaced ``DatetimeIndex`` if ``start``, ``stop``, and ``periods`` are specified, but ``freq`` is not. (:issue:`20808`)
527+
- :meth:`DatetimeIndex.set_freq` and :meth:`TimedeltaIndex.set_freq` are now available for setting the ``.freq`` attribute (:issue:`20886`)
527528

528529
.. _whatsnew_0230.api_breaking:
529530

@@ -998,6 +999,7 @@ Deprecations
998999
- Setting ``PeriodIndex.freq`` (which was not guaranteed to work correctly) is deprecated. Use :meth:`PeriodIndex.asfreq` instead (:issue:`20678`)
9991000
- ``Index.get_duplicates()`` is deprecated and will be removed in a future version (:issue:`20239`)
10001001
- The previous default behavior of negative indices in ``Categorical.take`` is deprecated. In a future version it will change from meaning missing values to meaning positional indices from the right. The future behavior is consistent with :meth:`Series.take` (:issue:`20664`).
1002+
- Setting the ``.freq`` attribute is deprecated for :class:`DatetimeIndex` and :class:`TimedeltaIndex`. Use the associated ``.set_freq()`` method instead (:issue:`20886`)
10011003

10021004

10031005
.. _whatsnew_0230.prior_deprecations:

pandas/core/indexes/datetimelike.py

+32
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,44 @@ def freq(self):
236236

237237
@freq.setter
238238
def freq(self, value):
239+
msg = ('Setting {obj}.freq has been deprecated and will be removed '
240+
'in a future version; use {obj}.set_freq instead.'
241+
).format(obj=type(self).__name__)
242+
warnings.warn(msg, FutureWarning, stacklevel=2)
239243
if value is not None:
240244
value = frequencies.to_offset(value)
241245
self._validate_frequency(self, value)
242246

243247
self._freq = value
244248

249+
def set_freq(self, freq):
250+
"""
251+
Set the frequency of the DatetimeIndex or TimedeltaIndex to the
252+
specified frequency `freq`.
253+
254+
Parameters
255+
----------
256+
freq: str or Offset
257+
The frequency to set on the DatetimeIndex or TimedeltaIndex
258+
259+
Returns
260+
-------
261+
new: DatetimeIndex or TimedeltaIndex with the new frequency
262+
263+
Raises
264+
------
265+
ValueError
266+
If the values of the DatetimeIndex or TimedeltaIndex are not
267+
compatible with the new frequency
268+
"""
269+
if freq is not None:
270+
freq = frequencies.to_offset(freq)
271+
self._validate_frequency(self, freq)
272+
273+
new = self.copy()
274+
new._freq = freq
275+
return new
276+
245277

246278
class DatetimeIndexOpsMixin(object):
247279
""" common ops mixin to support a unified interface datetimelike Index """

pandas/core/indexes/datetimes.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ class DatetimeIndex(DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin,
250250
normalize
251251
strftime
252252
snap
253+
set_freq
253254
tz_convert
254255
tz_localize
255256
round
@@ -460,7 +461,7 @@ def __new__(cls, data=None,
460461
if freq_infer:
461462
inferred = subarr.inferred_freq
462463
if inferred:
463-
subarr.freq = to_offset(inferred)
464+
subarr._freq = to_offset(inferred)
464465

465466
return subarr._deepcopy_if_needed(ref_to_data, copy)
466467

@@ -759,7 +760,7 @@ def _cached_range(cls, start=None, end=None, periods=None, freq=None,
759760
arr = tools.to_datetime(list(xdr), box=False)
760761

761762
cachedRange = DatetimeIndex._simple_new(arr)
762-
cachedRange.freq = freq
763+
cachedRange._freq = freq
763764
cachedRange = cachedRange.tz_localize(None)
764765
cachedRange.name = None
765766
drc[freq] = cachedRange
@@ -794,7 +795,7 @@ def _cached_range(cls, start=None, end=None, periods=None, freq=None,
794795

795796
indexSlice = cachedRange[startLoc:endLoc]
796797
indexSlice.name = name
797-
indexSlice.freq = freq
798+
indexSlice._freq = freq
798799

799800
return indexSlice
800801

@@ -1184,7 +1185,7 @@ def union(self, other):
11841185
result._tz = timezones.tz_standardize(this.tz)
11851186
if (result.freq is None and
11861187
(this.freq is not None or other.freq is not None)):
1187-
result.freq = to_offset(result.inferred_freq)
1188+
result._freq = to_offset(result.inferred_freq)
11881189
return result
11891190

11901191
def to_perioddelta(self, freq):
@@ -1232,7 +1233,7 @@ def union_many(self, others):
12321233
this._tz = timezones.tz_standardize(tz)
12331234

12341235
if this.freq is None:
1235-
this.freq = to_offset(this.inferred_freq)
1236+
this._freq = to_offset(this.inferred_freq)
12361237
return this
12371238

12381239
def join(self, other, how='left', level=None, return_indexers=False,
@@ -1393,7 +1394,7 @@ def intersection(self, other):
13931394
result = Index.intersection(self, other)
13941395
if isinstance(result, DatetimeIndex):
13951396
if result.freq is None:
1396-
result.freq = to_offset(result.inferred_freq)
1397+
result._freq = to_offset(result.inferred_freq)
13971398
return result
13981399

13991400
elif (other.freq is None or self.freq is None or
@@ -1404,7 +1405,7 @@ def intersection(self, other):
14041405
result = self._shallow_copy(result._values, name=result.name,
14051406
tz=result.tz, freq=None)
14061407
if result.freq is None:
1407-
result.freq = to_offset(result.inferred_freq)
1408+
result._freq = to_offset(result.inferred_freq)
14081409
return result
14091410

14101411
if len(self) == 0:
@@ -1738,9 +1739,13 @@ def offset(self):
17381739
def offset(self, value):
17391740
"""get/set the frequency of the Index"""
17401741
msg = ('DatetimeIndex.offset has been deprecated and will be removed '
1741-
'in a future version; use DatetimeIndex.freq instead.')
1742+
'in a future version; use DatetimeIndex.set_freq instead.')
17421743
warnings.warn(msg, FutureWarning, stacklevel=2)
1743-
self.freq = value
1744+
if value is not None:
1745+
value = to_offset(value)
1746+
self._validate_frequency(self, value)
1747+
1748+
self._freq = value
17441749

17451750
year = _field_accessor('year', 'Y', "The year of the datetime")
17461751
month = _field_accessor('month', 'M',

pandas/core/indexes/timedeltas.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ class TimedeltaIndex(DatetimeIndexOpsMixin, TimelikeOps, Int64Index):
149149
150150
Methods
151151
-------
152+
set_freq
152153
to_pytimedelta
153154
to_series
154155
round
@@ -253,14 +254,14 @@ def __new__(cls, data=None, unit=None, freq=None, start=None, end=None,
253254
if freq is not None and not freq_infer:
254255
index = cls._simple_new(data, name=name)
255256
cls._validate_frequency(index, freq)
256-
index.freq = freq
257+
index._freq = freq
257258
return index
258259

259260
if freq_infer:
260261
index = cls._simple_new(data, name=name)
261262
inferred = index.inferred_freq
262263
if inferred:
263-
index.freq = to_offset(inferred)
264+
index._freq = to_offset(inferred)
264265
return index
265266

266267
return cls._simple_new(data, name=name, freq=freq)
@@ -598,7 +599,7 @@ def union(self, other):
598599
result = Index.union(this, other)
599600
if isinstance(result, TimedeltaIndex):
600601
if result.freq is None:
601-
result.freq = to_offset(result.inferred_freq)
602+
result._freq = to_offset(result.inferred_freq)
602603
return result
603604

604605
def join(self, other, how='left', level=None, return_indexers=False,

pandas/core/resample.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -904,7 +904,7 @@ def _downsample(self, how, **kwargs):
904904
if not len(ax):
905905
# reset to the new freq
906906
obj = obj.copy()
907-
obj.index.freq = self.freq
907+
obj.index = obj.index.set_freq(self.freq)
908908
return obj
909909

910910
# do we have a regular frequency

pandas/tests/categorical/test_constructors.py

+22-27
Original file line numberDiff line numberDiff line change
@@ -256,36 +256,31 @@ def test_constructor_with_generator(self):
256256
cat = Categorical([0, 1, 2], categories=xrange(3))
257257
tm.assert_categorical_equal(cat, exp)
258258

259-
def test_constructor_with_datetimelike(self):
260-
259+
@pytest.mark.parametrize('dtl', [
260+
date_range('1995-01-01', periods=5, freq='s'),
261+
date_range('1995-01-01', periods=5, freq='s', tz='US/Eastern'),
262+
timedelta_range('1 day', periods=5, freq='s')])
263+
def test_constructor_with_datetimelike(self, dtl):
261264
# 12077
262265
# constructor wwth a datetimelike and NaT
266+
s = Series(dtl)
267+
c = Categorical(s)
268+
expected = dtl._constructor(s).set_freq(None)
269+
tm.assert_index_equal(c.categories, expected)
270+
tm.assert_numpy_array_equal(c.codes, np.arange(5, dtype='int8'))
271+
272+
# with NaT
273+
s2 = s.copy()
274+
s2.iloc[-1] = NaT
275+
c = Categorical(s2)
276+
expected = dtl._constructor(s2.dropna()).set_freq(None)
277+
tm.assert_index_equal(c.categories, expected)
278+
279+
exp = np.array([0, 1, 2, 3, -1], dtype=np.int8)
280+
tm.assert_numpy_array_equal(c.codes, exp)
263281

264-
for dtl in [date_range('1995-01-01 00:00:00', periods=5, freq='s'),
265-
date_range('1995-01-01 00:00:00', periods=5,
266-
freq='s', tz='US/Eastern'),
267-
timedelta_range('1 day', periods=5, freq='s')]:
268-
269-
s = Series(dtl)
270-
c = Categorical(s)
271-
expected = type(dtl)(s)
272-
expected.freq = None
273-
tm.assert_index_equal(c.categories, expected)
274-
tm.assert_numpy_array_equal(c.codes, np.arange(5, dtype='int8'))
275-
276-
# with NaT
277-
s2 = s.copy()
278-
s2.iloc[-1] = NaT
279-
c = Categorical(s2)
280-
expected = type(dtl)(s2.dropna())
281-
expected.freq = None
282-
tm.assert_index_equal(c.categories, expected)
283-
284-
exp = np.array([0, 1, 2, 3, -1], dtype=np.int8)
285-
tm.assert_numpy_array_equal(c.codes, exp)
286-
287-
result = repr(c)
288-
assert 'NaT' in result
282+
result = repr(c)
283+
assert 'NaT' in result
289284

290285
def test_constructor_from_index_series_datetimetz(self):
291286
idx = date_range('2015-01-01 10:00', freq='D', periods=3,

pandas/tests/indexes/datetimelike.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def test_map_dictlike(self, mapper):
6767

6868
# don't compare the freqs
6969
if isinstance(expected, pd.DatetimeIndex):
70-
expected.freq = None
70+
expected = expected.set_freq(None)
7171

7272
result = self.index.map(mapper(expected, self.index))
7373
tm.assert_index_equal(result, expected)
@@ -88,3 +88,15 @@ def test_asobject_deprecated(self):
8888
with tm.assert_produces_warning(FutureWarning):
8989
i = d.asobject
9090
assert isinstance(i, pd.Index)
91+
92+
def test_freq_setter_deprecated(self):
93+
# GH 20678/20886
94+
idx = self.create_index()
95+
96+
# no warning for getter
97+
with tm.assert_produces_warning(None):
98+
idx.freq
99+
100+
# warning for setter
101+
with tm.assert_produces_warning(FutureWarning):
102+
idx.freq = pd.offsets.Day()

pandas/tests/indexes/datetimes/test_date_range.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ def test_misc(self):
396396
assert len(dr) == 20
397397
assert dr[0] == firstDate
398398
assert dr[-1] == end
399+
assert dr.freq == BDay()
399400

400401
def test_date_parse_failure(self):
401402
badly_formed_date = '2007/100/1'
@@ -416,7 +417,6 @@ def test_daterange_bug_456(self):
416417
# GH #456
417418
rng1 = bdate_range('12/5/2011', '12/5/2011')
418419
rng2 = bdate_range('12/2/2011', '12/5/2011')
419-
rng2.freq = BDay()
420420

421421
result = rng1.union(rng2)
422422
assert isinstance(result, DatetimeIndex)
@@ -658,12 +658,12 @@ def test_misc(self):
658658
assert len(dr) == 20
659659
assert dr[0] == firstDate
660660
assert dr[-1] == end
661+
assert dr.freq == CDay()
661662

662663
def test_daterange_bug_456(self):
663664
# GH #456
664665
rng1 = bdate_range('12/5/2011', '12/5/2011', freq='C')
665666
rng2 = bdate_range('12/2/2011', '12/5/2011', freq='C')
666-
rng2.freq = CDay()
667667

668668
result = rng1.union(rng2)
669669
assert isinstance(result, DatetimeIndex)

pandas/tests/indexes/datetimes/test_ops.py

+44-6
Original file line numberDiff line numberDiff line change
@@ -406,37 +406,75 @@ def test_equals(self):
406406
assert not idx.equals(list(idx3))
407407
assert not idx.equals(pd.Series(idx3))
408408

409+
@pytest.mark.parametrize('values', [
410+
['20180101', '20180103', '20180105'], []])
411+
@pytest.mark.parametrize('freq', [
412+
'2D', Day(2), '2B', BDay(2), '48H', Hour(48)])
413+
@pytest.mark.parametrize('tz', [None, 'US/Eastern'])
414+
def test_set_freq(self, values, freq, tz):
415+
# GH 20886
416+
idx = DatetimeIndex(values, tz=tz)
417+
418+
# can set to an offset, converting from string if necessary
419+
idx = idx.set_freq(freq)
420+
assert idx.freq == freq
421+
assert isinstance(idx.freq, ABCDateOffset)
422+
423+
# can reset to None
424+
idx = idx.set_freq(None)
425+
assert idx.freq is None
426+
427+
def test_set_freq_errors(self):
428+
# GH 20886
429+
idx = DatetimeIndex(['20180101', '20180103', '20180105'])
430+
431+
# setting with an incompatible freq
432+
msg = ('Inferred frequency 2D from passed values does not conform to '
433+
'passed frequency 5D')
434+
with tm.assert_raises_regex(ValueError, msg):
435+
idx.set_freq('5D')
436+
437+
# setting with non-freq string
438+
with tm.assert_raises_regex(ValueError, 'Invalid frequency'):
439+
idx.set_freq('foo')
440+
409441
@pytest.mark.parametrize('values', [
410442
['20180101', '20180103', '20180105'], []])
411443
@pytest.mark.parametrize('freq', [
412444
'2D', Day(2), '2B', BDay(2), '48H', Hour(48)])
413445
@pytest.mark.parametrize('tz', [None, 'US/Eastern'])
414446
def test_freq_setter(self, values, freq, tz):
415-
# GH 20678
447+
# GH 20678/20886
416448
idx = DatetimeIndex(values, tz=tz)
417449

418450
# can set to an offset, converting from string if necessary
419-
idx.freq = freq
451+
with tm.assert_produces_warning(FutureWarning):
452+
idx.freq = freq
453+
420454
assert idx.freq == freq
421455
assert isinstance(idx.freq, ABCDateOffset)
422456

423457
# can reset to None
424-
idx.freq = None
458+
with tm.assert_produces_warning(FutureWarning):
459+
idx.freq = None
460+
425461
assert idx.freq is None
426462

427463
def test_freq_setter_errors(self):
428-
# GH 20678
464+
# GH 20678/20886
429465
idx = DatetimeIndex(['20180101', '20180103', '20180105'])
430466

431467
# setting with an incompatible freq
432468
msg = ('Inferred frequency 2D from passed values does not conform to '
433469
'passed frequency 5D')
434470
with tm.assert_raises_regex(ValueError, msg):
435-
idx.freq = '5D'
471+
with tm.assert_produces_warning(FutureWarning):
472+
idx.freq = '5D'
436473

437474
# setting with non-freq string
438475
with tm.assert_raises_regex(ValueError, 'Invalid frequency'):
439-
idx.freq = 'foo'
476+
with tm.assert_produces_warning(FutureWarning):
477+
idx.freq = 'foo'
440478

441479
def test_offset_deprecated(self):
442480
# GH 20716

pandas/tests/indexes/datetimes/test_setops.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,7 @@ def test_union_bug_4564(self):
9191

9292
def test_union_freq_both_none(self):
9393
# GH11086
94-
expected = bdate_range('20150101', periods=10)
95-
expected.freq = None
96-
94+
expected = bdate_range('20150101', periods=10).set_freq(None)
9795
result = expected.union(expected)
9896
tm.assert_index_equal(result, expected)
9997
assert result.freq is None

0 commit comments

Comments
 (0)