From 2fca7751205267bcc3fa1c07de9af19d646b5f6a Mon Sep 17 00:00:00 2001 From: jschendel Date: Sun, 29 Apr 2018 11:16:32 -0600 Subject: [PATCH] Deprecate TDI/DTI freq setter, implement asfreq --- doc/source/whatsnew/v0.23.0.txt | 5 +- pandas/core/indexes/datetimelike.py | 30 +++++++++++ pandas/core/indexes/datetimes.py | 23 +++++---- pandas/core/indexes/timedeltas.py | 7 +-- pandas/core/resample.py | 2 +- pandas/tests/categorical/test_constructors.py | 51 +++++++++---------- pandas/tests/indexes/datetimelike.py | 14 ++++- .../indexes/datetimes/test_date_range.py | 4 +- pandas/tests/indexes/datetimes/test_ops.py | 46 +++++++++++++++-- pandas/tests/indexes/datetimes/test_setops.py | 2 +- pandas/tests/indexes/period/test_ops.py | 12 ----- pandas/tests/indexes/timedeltas/test_ops.py | 51 +++++++++++++++++-- pandas/tests/test_resample.py | 2 +- 13 files changed, 181 insertions(+), 68 deletions(-) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 0b2139aa1bf9ac..30ede0356ee49b 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -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: @@ -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: diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 158b272384ae86..500ae515f5e507 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -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 """ diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 720718e78d50ec..b1afa16540c3ef 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -247,6 +247,7 @@ class DatetimeIndex(DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin, Methods ------- + asfreq normalize strftime snap @@ -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) @@ -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 @@ -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 @@ -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): @@ -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, @@ -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 @@ -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: @@ -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', diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 6b278fc35c831e..d731be2ad5f46c 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -149,6 +149,7 @@ class TimedeltaIndex(DatetimeIndexOpsMixin, TimelikeOps, Int64Index): Methods ------- + asfreq to_pytimedelta to_series round @@ -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) @@ -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, diff --git a/pandas/core/resample.py b/pandas/core/resample.py index bc7871a0d75c17..8c297b194eda86 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -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 diff --git a/pandas/tests/categorical/test_constructors.py b/pandas/tests/categorical/test_constructors.py index 6cc34770a65e0b..aa55b268d118b6 100644 --- a/pandas/tests/categorical/test_constructors.py +++ b/pandas/tests/categorical/test_constructors.py @@ -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, diff --git a/pandas/tests/indexes/datetimelike.py b/pandas/tests/indexes/datetimelike.py index 7d01a2a70145d5..f976bd15e037c6 100644 --- a/pandas/tests/indexes/datetimelike.py +++ b/pandas/tests/indexes/datetimelike.py @@ -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) @@ -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() diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py index e5291ed52a86c6..33b5b56676defe 100644 --- a/pandas/tests/indexes/datetimes/test_date_range.py +++ b/pandas/tests/indexes/datetimes/test_date_range.py @@ -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' @@ -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() result = rng1.union(rng2) assert isinstance(result, DatetimeIndex) @@ -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() result = rng1.union(rng2) assert isinstance(result, DatetimeIndex) diff --git a/pandas/tests/indexes/datetimes/test_ops.py b/pandas/tests/indexes/datetimes/test_ops.py index c6334e70a1d2c2..140e39f9e2e8c0 100644 --- a/pandas/tests/indexes/datetimes/test_ops.py +++ b/pandas/tests/indexes/datetimes/test_ops.py @@ -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', [ @@ -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): @@ -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 diff --git a/pandas/tests/indexes/datetimes/test_setops.py b/pandas/tests/indexes/datetimes/test_setops.py index cb9364edc0cc3d..0d9aa4a4d7e8aa 100644 --- a/pandas/tests/indexes/datetimes/test_setops.py +++ b/pandas/tests/indexes/datetimes/test_setops.py @@ -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) diff --git a/pandas/tests/indexes/period/test_ops.py b/pandas/tests/indexes/period/test_ops.py index 85aa3f6a38fb30..7d117b0b626cf9 100644 --- a/pandas/tests/indexes/period/test_ops.py +++ b/pandas/tests/indexes/period/test_ops.py @@ -401,18 +401,6 @@ def test_equals(self, freq): assert not idx.equals(list(idx3)) assert not idx.equals(pd.Series(idx3)) - def test_freq_setter_deprecated(self): - # GH 20678 - idx = pd.period_range('2018Q1', periods=4, freq='Q') - - # 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() - class TestPeriodIndexSeriesMethods(object): """ Test PeriodIndex and Period Series Ops consistency """ diff --git a/pandas/tests/indexes/timedeltas/test_ops.py b/pandas/tests/indexes/timedeltas/test_ops.py index 2e257bb8a500a9..e0e78495834c66 100644 --- a/pandas/tests/indexes/timedeltas/test_ops.py +++ b/pandas/tests/indexes/timedeltas/test_ops.py @@ -308,6 +308,40 @@ def test_equals(self): assert not idx.equals(list(idx2)) assert not idx.equals(pd.Series(idx2)) + @pytest.mark.parametrize('values', [['0 days', '2 days', '4 days'], []]) + @pytest.mark.parametrize('freq', ['2D', Day(2), '48H', Hour(48)]) + def test_asfreq(self, values, freq): + # GH 20678 + idx = TimedeltaIndex(values) + + # 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 = TimedeltaIndex(['0 days', '2 days', '4 days']) + + # 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 a non-fixed frequency + msg = '<2 \* BusinessDays> is a non-fixed frequency' + with tm.assert_raises_regex(ValueError, msg): + idx.asfreq('2B') + + # setting with non-freq string + with tm.assert_raises_regex(ValueError, 'Invalid frequency'): + idx.asfreq('foo') + @pytest.mark.parametrize('values', [['0 days', '2 days', '4 days'], []]) @pytest.mark.parametrize('freq', ['2D', Day(2), '48H', Hour(48)]) def test_freq_setter(self, values, freq): @@ -315,12 +349,16 @@ def test_freq_setter(self, values, freq): idx = TimedeltaIndex(values) # 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): @@ -331,16 +369,19 @@ 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 a non-fixed frequency msg = '<2 \* BusinessDays> is a non-fixed frequency' with tm.assert_raises_regex(ValueError, msg): - idx.freq = '2B' + with tm.assert_produces_warning(FutureWarning): + idx.freq = '2B' # 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' class TestTimedeltas(object): diff --git a/pandas/tests/test_resample.py b/pandas/tests/test_resample.py index 778ea73b3ef256..fab641962ea8f5 100644 --- a/pandas/tests/test_resample.py +++ b/pandas/tests/test_resample.py @@ -624,7 +624,7 @@ def test_asfreq(self, series_and_frame, freq): result = obj.resample(freq).asfreq() if freq == '2D': new_index = obj.index.take(np.arange(0, len(obj.index), 2)) - new_index.freq = to_offset('2D') + new_index._freq = to_offset('2D') else: new_index = self.create_index(obj.index[0], obj.index[-1], freq=freq)