Skip to content

Commit 53db8fb

Browse files
committed
API: Implement set_freq for DTI/TDI, deprecate freq setter
1 parent f799916 commit 53db8fb

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
@@ -450,6 +450,7 @@ Other Enhancements
450450
- Updated :meth:`DataFrame.to_gbq` and :meth:`pandas.read_gbq` signature and documentation to reflect changes from
451451
the Pandas-GBQ library version 0.4.0. Adds intersphinx mapping to Pandas-GBQ
452452
library. (:issue:`20564`)
453+
- :meth:`DatetimeIndex.set_freq` and :meth:`TimedeltaIndex.set_freq` are now available for setting the ``.freq`` attribute (:issue:`20886`)
453454

454455
.. _whatsnew_0230.api_breaking:
455456

@@ -893,6 +894,7 @@ Deprecations
893894
- Setting ``PeriodIndex.freq`` (which was not guaranteed to work correctly) is deprecated. Use :meth:`PeriodIndex.asfreq` instead (:issue:`20678`)
894895
- ``Index.get_duplicates()`` is deprecated and will be removed in a future version (:issue:`20239`)
895896
- 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`).
897+
- Setting the ``.freq`` attribute is deprecated for :class:`DatetimeIndex` and :class:`TimedeltaIndex`. Use the associated ``.set_freq()`` method instead (:issue:`20886`)
896898

897899

898900
.. _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
@@ -459,7 +460,7 @@ def __new__(cls, data=None,
459460
if freq_infer:
460461
inferred = subarr.inferred_freq
461462
if inferred:
462-
subarr.freq = to_offset(inferred)
463+
subarr._freq = to_offset(inferred)
463464

464465
return subarr._deepcopy_if_needed(ref_to_data, copy)
465466

@@ -751,7 +752,7 @@ def _cached_range(cls, start=None, end=None, periods=None, freq=None,
751752
arr = tools.to_datetime(list(xdr), box=False)
752753

753754
cachedRange = DatetimeIndex._simple_new(arr)
754-
cachedRange.freq = freq
755+
cachedRange._freq = freq
755756
cachedRange = cachedRange.tz_localize(None)
756757
cachedRange.name = None
757758
drc[freq] = cachedRange
@@ -786,7 +787,7 @@ def _cached_range(cls, start=None, end=None, periods=None, freq=None,
786787

787788
indexSlice = cachedRange[startLoc:endLoc]
788789
indexSlice.name = name
789-
indexSlice.freq = freq
790+
indexSlice._freq = freq
790791

791792
return indexSlice
792793

@@ -1176,7 +1177,7 @@ def union(self, other):
11761177
result._tz = timezones.tz_standardize(this.tz)
11771178
if (result.freq is None and
11781179
(this.freq is not None or other.freq is not None)):
1179-
result.freq = to_offset(result.inferred_freq)
1180+
result._freq = to_offset(result.inferred_freq)
11801181
return result
11811182

11821183
def to_perioddelta(self, freq):
@@ -1224,7 +1225,7 @@ def union_many(self, others):
12241225
this._tz = timezones.tz_standardize(tz)
12251226

12261227
if this.freq is None:
1227-
this.freq = to_offset(this.inferred_freq)
1228+
this._freq = to_offset(this.inferred_freq)
12281229
return this
12291230

12301231
def join(self, other, how='left', level=None, return_indexers=False,
@@ -1385,7 +1386,7 @@ def intersection(self, other):
13851386
result = Index.intersection(self, other)
13861387
if isinstance(result, DatetimeIndex):
13871388
if result.freq is None:
1388-
result.freq = to_offset(result.inferred_freq)
1389+
result._freq = to_offset(result.inferred_freq)
13891390
return result
13901391

13911392
elif (other.freq is None or self.freq is None or
@@ -1396,7 +1397,7 @@ def intersection(self, other):
13961397
result = self._shallow_copy(result._values, name=result.name,
13971398
tz=result.tz, freq=None)
13981399
if result.freq is None:
1399-
result.freq = to_offset(result.inferred_freq)
1400+
result._freq = to_offset(result.inferred_freq)
14001401
return result
14011402

14021403
if len(self) == 0:
@@ -1730,9 +1731,13 @@ def offset(self):
17301731
def offset(self, value):
17311732
"""get/set the frequency of the Index"""
17321733
msg = ('DatetimeIndex.offset has been deprecated and will be removed '
1733-
'in a future version; use DatetimeIndex.freq instead.')
1734+
'in a future version; use DatetimeIndex.set_freq instead.')
17341735
warnings.warn(msg, FutureWarning, stacklevel=2)
1735-
self.freq = value
1736+
if value is not None:
1737+
value = to_offset(value)
1738+
self._validate_frequency(self, value)
1739+
1740+
self._freq = value
17361741

17371742
year = _field_accessor('year', 'Y', "The year of the datetime")
17381743
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
@@ -62,7 +62,7 @@ def test_map_dictlike(self, mapper):
6262

6363
# don't compare the freqs
6464
if isinstance(expected, pd.DatetimeIndex):
65-
expected.freq = None
65+
expected = expected.set_freq(None)
6666

6767
result = self.index.map(mapper(expected, self.index))
6868
tm.assert_index_equal(result, expected)
@@ -83,3 +83,15 @@ def test_asobject_deprecated(self):
8383
with tm.assert_produces_warning(FutureWarning):
8484
i = d.asobject
8585
assert isinstance(i, pd.Index)
86+
87+
def test_freq_setter_deprecated(self):
88+
# GH 20678/20886
89+
idx = self.create_index()
90+
91+
# no warning for getter
92+
with tm.assert_produces_warning(None):
93+
idx.freq
94+
95+
# warning for setter
96+
with tm.assert_produces_warning(FutureWarning):
97+
idx.freq = pd.offsets.Day()

pandas/tests/indexes/datetimes/test_date_range.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ def test_misc(self):
379379
assert len(dr) == 20
380380
assert dr[0] == firstDate
381381
assert dr[-1] == end
382+
assert dr.freq == BDay()
382383

383384
def test_date_parse_failure(self):
384385
badly_formed_date = '2007/100/1'
@@ -399,7 +400,6 @@ def test_daterange_bug_456(self):
399400
# GH #456
400401
rng1 = bdate_range('12/5/2011', '12/5/2011')
401402
rng2 = bdate_range('12/2/2011', '12/5/2011')
402-
rng2.freq = BDay()
403403

404404
result = rng1.union(rng2)
405405
assert isinstance(result, DatetimeIndex)
@@ -641,12 +641,12 @@ def test_misc(self):
641641
assert len(dr) == 20
642642
assert dr[0] == firstDate
643643
assert dr[-1] == end
644+
assert dr.freq == CDay()
644645

645646
def test_daterange_bug_456(self):
646647
# GH #456
647648
rng1 = bdate_range('12/5/2011', '12/5/2011', freq='C')
648649
rng2 = bdate_range('12/2/2011', '12/5/2011', freq='C')
649-
rng2.freq = CDay()
650650

651651
result = rng1.union(rng2)
652652
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)