Skip to content

Commit

Permalink
Deprecate TDI/DTI freq setter, implement asfreq
Browse files Browse the repository at this point in the history
  • Loading branch information
jschendel committed Apr 29, 2018
1 parent 8fd141f commit 2fca775
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 68 deletions.
5 changes: 3 additions & 2 deletions doc/source/whatsnew/v0.23.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ Other Enhancements
- Updated :meth:`DataFrame.to_gbq` and :meth:`pandas.read_gbq` signature and documentation to reflect changes from
the Pandas-GBQ library version 0.4.0. Adds intersphinx mapping to Pandas-GBQ
library. (:issue:`20564`)
- :meth:`DatetimeIndex.asfreq` and :meth:`TimedeltaIndex.asfreq` are now available for setting the ``.freq`` attribute (:issue:`20678`)

.. _whatsnew_0230.api_breaking:

Expand Down Expand Up @@ -889,9 +890,9 @@ Deprecations
- The ``data``, ``base``, ``strides``, ``flags`` and ``itemsize`` properties
of the ``Series`` and ``Index`` classes have been deprecated and will be
removed in a future version (:issue:`20419`).
- ``DatetimeIndex.offset`` is deprecated. Use ``DatetimeIndex.freq`` instead (:issue:`20716`)
- ``DatetimeIndex.offset`` is deprecated. Use meth:`DatetimeIndex.asfreq` instead (:issue:`20716`)
- ``Index.get_duplicates()`` is deprecated and will be removed in a future version (:issue:`20239`)
- Setting ``PeriodIndex.freq`` is deprecated. Use :meth:`PeriodIndex.asfreq` instead (:issue:`20678`)
- Setting the ``.freq`` attribute is deprecated for :class:`DatetimeIndex`, :class:`TimedeltaIndex`, and :class:`PeriodIndex`. Use the associated ``.asfreq()`` method instead (:issue:`20678`)

.. _whatsnew_0230.prior_deprecations:

Expand Down
30 changes: 30 additions & 0 deletions pandas/core/indexes/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,42 @@ def freq(self):

@freq.setter
def freq(self, value):
msg = ('Setting {obj}.freq has been deprecated and will be removed '
'in a future version; use {obj}.asfreq instead.'
).format(obj=type(self).__name__)
warnings.warn(msg, FutureWarning, stacklevel=2)
if value is not None:
value = frequencies.to_offset(value)
self._validate_frequency(self, value)

self._freq = value

def asfreq(self, freq):
"""
Set the frequency of the DatetimeIndex or TimedeltaIndex to the
specified frequency `freq`.
Parameters
----------
freq: str or Offset
The frequency to set on the DatetimeIndex or TimedeltaIndex
Returns
-------
new: DatetimeIndex or TimedeltaIndex with the new frequency
Raises
------
ValueError
If the values of the DatetimeIndex or TimedeltaIndex are not
compatible with the new frequency
"""
if freq is None:
new = self.copy()
new._freq = None
return new
return self._constructor(self, freq=freq)


class DatetimeIndexOpsMixin(object):
""" common ops mixin to support a unified interface datetimelike Index """
Expand Down
23 changes: 14 additions & 9 deletions pandas/core/indexes/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ class DatetimeIndex(DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin,
Methods
-------
asfreq
normalize
strftime
snap
Expand Down Expand Up @@ -459,7 +460,7 @@ def __new__(cls, data=None,
if freq_infer:
inferred = subarr.inferred_freq
if inferred:
subarr.freq = to_offset(inferred)
subarr._freq = to_offset(inferred)

return subarr._deepcopy_if_needed(ref_to_data, copy)

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

cachedRange = DatetimeIndex._simple_new(arr)
cachedRange.freq = freq
cachedRange._freq = freq
cachedRange = cachedRange.tz_localize(None)
cachedRange.name = None
drc[freq] = cachedRange
Expand Down Expand Up @@ -786,7 +787,7 @@ def _cached_range(cls, start=None, end=None, periods=None, freq=None,

indexSlice = cachedRange[startLoc:endLoc]
indexSlice.name = name
indexSlice.freq = freq
indexSlice._freq = freq

return indexSlice

Expand Down Expand Up @@ -1176,7 +1177,7 @@ def union(self, other):
result._tz = timezones.tz_standardize(this.tz)
if (result.freq is None and
(this.freq is not None or other.freq is not None)):
result.freq = to_offset(result.inferred_freq)
result._freq = to_offset(result.inferred_freq)
return result

def to_perioddelta(self, freq):
Expand Down Expand Up @@ -1224,7 +1225,7 @@ def union_many(self, others):
this._tz = timezones.tz_standardize(tz)

if this.freq is None:
this.freq = to_offset(this.inferred_freq)
this._freq = to_offset(this.inferred_freq)
return this

def join(self, other, how='left', level=None, return_indexers=False,
Expand Down Expand Up @@ -1385,7 +1386,7 @@ def intersection(self, other):
result = Index.intersection(self, other)
if isinstance(result, DatetimeIndex):
if result.freq is None:
result.freq = to_offset(result.inferred_freq)
result._freq = to_offset(result.inferred_freq)
return result

elif (other.freq is None or self.freq is None or
Expand All @@ -1396,7 +1397,7 @@ def intersection(self, other):
result = self._shallow_copy(result._values, name=result.name,
tz=result.tz, freq=None)
if result.freq is None:
result.freq = to_offset(result.inferred_freq)
result._freq = to_offset(result.inferred_freq)
return result

if len(self) == 0:
Expand Down Expand Up @@ -1730,9 +1731,13 @@ def offset(self):
def offset(self, value):
"""get/set the frequency of the Index"""
msg = ('DatetimeIndex.offset has been deprecated and will be removed '
'in a future version; use DatetimeIndex.freq instead.')
'in a future version; use DatetimeIndex.asfreq instead.')
warnings.warn(msg, FutureWarning, stacklevel=2)
self.freq = value
if value is not None:
value = to_offset(value)
self._validate_frequency(self, value)

self._freq = value

year = _field_accessor('year', 'Y', "The year of the datetime")
month = _field_accessor('month', 'M',
Expand Down
7 changes: 4 additions & 3 deletions pandas/core/indexes/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ class TimedeltaIndex(DatetimeIndexOpsMixin, TimelikeOps, Int64Index):
Methods
-------
asfreq
to_pytimedelta
to_series
round
Expand Down Expand Up @@ -253,14 +254,14 @@ def __new__(cls, data=None, unit=None, freq=None, start=None, end=None,
if freq is not None and not freq_infer:
index = cls._simple_new(data, name=name)
cls._validate_frequency(index, freq)
index.freq = freq
index._freq = freq
return index

if freq_infer:
index = cls._simple_new(data, name=name)
inferred = index.inferred_freq
if inferred:
index.freq = to_offset(inferred)
index._freq = to_offset(inferred)
return index

return cls._simple_new(data, name=name, freq=freq)
Expand Down Expand Up @@ -598,7 +599,7 @@ def union(self, other):
result = Index.union(this, other)
if isinstance(result, TimedeltaIndex):
if result.freq is None:
result.freq = to_offset(result.inferred_freq)
result._freq = to_offset(result.inferred_freq)
return result

def join(self, other, how='left', level=None, return_indexers=False,
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,7 @@ def _downsample(self, how, **kwargs):
if not len(ax):
# reset to the new freq
obj = obj.copy()
obj.index.freq = self.freq
obj.index._freq = self.freq
return obj

# do we have a regular frequency
Expand Down
51 changes: 24 additions & 27 deletions pandas/tests/categorical/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,36 +256,33 @@ def test_constructor_with_generator(self):
cat = Categorical([0, 1, 2], categories=xrange(3))
tm.assert_categorical_equal(cat, exp)

def test_constructor_with_datetimelike(self):

@pytest.mark.parametrize('dtl', [
date_range('1995-01-01', periods=5, freq='s'),
date_range('1995-01-01', periods=5, freq='s', tz='US/Eastern'),
timedelta_range('1 day', periods=5, freq='s')])
def test_constructor_with_datetimelike(self, dtl):
# 12077
# constructor wwth a datetimelike and NaT
s = Series(dtl)
c = Categorical(s)
expected = type(dtl)(s)
expected._freq = None
tm.assert_index_equal(c.categories, expected)
tm.assert_numpy_array_equal(c.codes, np.arange(5, dtype='int8'))

# with NaT
s2 = s.copy()
s2.iloc[-1] = NaT
c = Categorical(s2)
expected = type(dtl)(s2.dropna())
expected._freq = None
tm.assert_index_equal(c.categories, expected)

exp = np.array([0, 1, 2, 3, -1], dtype=np.int8)
tm.assert_numpy_array_equal(c.codes, exp)

for dtl in [date_range('1995-01-01 00:00:00', periods=5, freq='s'),
date_range('1995-01-01 00:00:00', periods=5,
freq='s', tz='US/Eastern'),
timedelta_range('1 day', periods=5, freq='s')]:

s = Series(dtl)
c = Categorical(s)
expected = type(dtl)(s)
expected.freq = None
tm.assert_index_equal(c.categories, expected)
tm.assert_numpy_array_equal(c.codes, np.arange(5, dtype='int8'))

# with NaT
s2 = s.copy()
s2.iloc[-1] = NaT
c = Categorical(s2)
expected = type(dtl)(s2.dropna())
expected.freq = None
tm.assert_index_equal(c.categories, expected)

exp = np.array([0, 1, 2, 3, -1], dtype=np.int8)
tm.assert_numpy_array_equal(c.codes, exp)

result = repr(c)
assert 'NaT' in result
result = repr(c)
assert 'NaT' in result

def test_constructor_from_index_series_datetimetz(self):
idx = date_range('2015-01-01 10:00', freq='D', periods=3,
Expand Down
14 changes: 13 additions & 1 deletion pandas/tests/indexes/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def test_map_dictlike(self, mapper):

# don't compare the freqs
if isinstance(expected, pd.DatetimeIndex):
expected.freq = None
expected._freq = None

result = self.index.map(mapper(expected, self.index))
tm.assert_index_equal(result, expected)
Expand All @@ -83,3 +83,15 @@ def test_asobject_deprecated(self):
with tm.assert_produces_warning(FutureWarning):
i = d.asobject
assert isinstance(i, pd.Index)

def test_freq_setter_deprecated(self):
# GH 20678
idx = self.create_index()

# no warning for getter
with tm.assert_produces_warning(None):
idx.freq

# warning for setter
with tm.assert_produces_warning(FutureWarning):
idx.freq = pd.offsets.Day()
4 changes: 2 additions & 2 deletions pandas/tests/indexes/datetimes/test_date_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ def test_misc(self):
assert len(dr) == 20
assert dr[0] == firstDate
assert dr[-1] == end
assert dr.freq == BDay()

def test_date_parse_failure(self):
badly_formed_date = '2007/100/1'
Expand All @@ -399,7 +400,6 @@ def test_daterange_bug_456(self):
# GH #456
rng1 = bdate_range('12/5/2011', '12/5/2011')
rng2 = bdate_range('12/2/2011', '12/5/2011')
rng2.freq = BDay()

This comment has been minimized.

Copy link
@jschendel

jschendel Apr 29, 2018

Author Owner

This is redundant, as rng2 should already have this as the frequency. Added a check for this in a separate test a few lines above to enforce this, since I didn't see any existing tests to verify this.


result = rng1.union(rng2)
assert isinstance(result, DatetimeIndex)
Expand Down Expand Up @@ -641,12 +641,12 @@ def test_misc(self):
assert len(dr) == 20
assert dr[0] == firstDate
assert dr[-1] == end
assert dr.freq == CDay()

def test_daterange_bug_456(self):
# GH #456
rng1 = bdate_range('12/5/2011', '12/5/2011', freq='C')
rng2 = bdate_range('12/2/2011', '12/5/2011', freq='C')
rng2.freq = CDay()

This comment has been minimized.

Copy link
@jschendel

jschendel Apr 29, 2018

Author Owner

same


result = rng1.union(rng2)
assert isinstance(result, DatetimeIndex)
Expand Down
46 changes: 42 additions & 4 deletions pandas/tests/indexes/datetimes/test_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,38 @@ def test_equals(self):
assert not idx.equals(list(idx3))
assert not idx.equals(pd.Series(idx3))

@pytest.mark.parametrize('values', [
['20180101', '20180103', '20180105'], []])
@pytest.mark.parametrize('freq', [
'2D', Day(2), '2B', BDay(2), '48H', Hour(48)])
@pytest.mark.parametrize('tz', [None, 'US/Eastern'])
def test_asfreq(self, values, freq, tz):
# GH 20678
idx = DatetimeIndex(values, tz=tz)

# can set to an offset, converting from string if necessary
idx = idx.asfreq(freq)
assert idx.freq == freq
assert isinstance(idx.freq, ABCDateOffset)

# can reset to None
idx = idx.asfreq(None)
assert idx.freq is None

def test_asfreq_errors(self):
# GH 20678
idx = DatetimeIndex(['20180101', '20180103', '20180105'])

# setting with an incompatible freq
msg = ('Inferred frequency 2D from passed values does not conform to '
'passed frequency 5D')
with tm.assert_raises_regex(ValueError, msg):
idx.asfreq('5D')

# setting with non-freq string
with tm.assert_raises_regex(ValueError, 'Invalid frequency'):
idx.asfreq('foo')

@pytest.mark.parametrize('values', [
['20180101', '20180103', '20180105'], []])
@pytest.mark.parametrize('freq', [
Expand All @@ -416,12 +448,16 @@ def test_freq_setter(self, values, freq, tz):
idx = DatetimeIndex(values, tz=tz)

# can set to an offset, converting from string if necessary
idx.freq = freq
with tm.assert_produces_warning(FutureWarning):
idx.freq = freq

assert idx.freq == freq
assert isinstance(idx.freq, ABCDateOffset)

# can reset to None
idx.freq = None
with tm.assert_produces_warning(FutureWarning):
idx.freq = None

assert idx.freq is None

def test_freq_setter_errors(self):
Expand All @@ -432,11 +468,13 @@ def test_freq_setter_errors(self):
msg = ('Inferred frequency 2D from passed values does not conform to '
'passed frequency 5D')
with tm.assert_raises_regex(ValueError, msg):
idx.freq = '5D'
with tm.assert_produces_warning(FutureWarning):
idx.freq = '5D'

# setting with non-freq string
with tm.assert_raises_regex(ValueError, 'Invalid frequency'):
idx.freq = 'foo'
with tm.assert_produces_warning(FutureWarning):
idx.freq = 'foo'

def test_offset_deprecated(self):
# GH 20716
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/indexes/datetimes/test_setops.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def test_union_bug_4564(self):
def test_union_freq_both_none(self):
# GH11086
expected = bdate_range('20150101', periods=10)
expected.freq = None
expected._freq = None

result = expected.union(expected)
tm.assert_index_equal(result, expected)
Expand Down
Loading

0 comments on commit 2fca775

Please sign in to comment.