From e68468c4ce38ad1a4f41b55fb49d16fa7716b6dc Mon Sep 17 00:00:00 2001 From: Iva Miholic Date: Sun, 16 Jul 2017 09:04:35 +0100 Subject: [PATCH 1/2] ENH: Create a 'Y' alias for end of year Closes gh-9313 --- pandas/tests/tseries/test_frequencies.py | 41 ++++++++++++++++-------- pandas/tseries/frequencies.py | 22 +++++++++++++ 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/pandas/tests/tseries/test_frequencies.py b/pandas/tests/tseries/test_frequencies.py index 54d12317b0bf8..4bcd0b49db7e0 100644 --- a/pandas/tests/tseries/test_frequencies.py +++ b/pandas/tests/tseries/test_frequencies.py @@ -248,9 +248,10 @@ def test_anchored_shortcuts(self): # ensure invalid cases fail as expected invalid_anchors = ['SM-0', 'SM-28', 'SM-29', - 'SM-FOO', 'BSM', 'SM--1' + 'SM-FOO', 'BSM', 'SM--1', 'SMS-1', 'SMS-28', 'SMS-30', - 'SMS-BAR', 'BSMS', 'SMS--2'] + 'SMS-BAR', 'SMS-BYR' 'BSMS', + 'SMS--2'] for invalid_anchor in invalid_anchors: with tm.assert_raises_regex(ValueError, 'Invalid frequency: '): @@ -292,11 +293,15 @@ def test_get_rule_month(): result = frequencies._get_rule_month('A-DEC') assert (result == 'DEC') + result = frequencies._get_rule_month('Y-DEC') + assert (result == 'DEC') result = frequencies._get_rule_month(offsets.YearEnd()) assert (result == 'DEC') result = frequencies._get_rule_month('A-MAY') assert (result == 'MAY') + result = frequencies._get_rule_month('Y-MAY') + assert (result == 'MAY') result = frequencies._get_rule_month(offsets.YearEnd(month=5)) assert (result == 'MAY') @@ -305,6 +310,10 @@ def test_period_str_to_code(): assert (frequencies._period_str_to_code('A') == 1000) assert (frequencies._period_str_to_code('A-DEC') == 1000) assert (frequencies._period_str_to_code('A-JAN') == 1001) + assert (frequencies._period_str_to_code('Y') == 1000) + assert (frequencies._period_str_to_code('Y-DEC') == 1000) + assert (frequencies._period_str_to_code('Y-JAN') == 1001) + assert (frequencies._period_str_to_code('Q') == 2000) assert (frequencies._period_str_to_code('Q-DEC') == 2000) assert (frequencies._period_str_to_code('Q-FEB') == 2002) @@ -349,6 +358,10 @@ def test_freq_code(self): assert frequencies.get_freq('3A') == 1000 assert frequencies.get_freq('-1A') == 1000 + assert frequencies.get_freq('Y') == 1000 + assert frequencies.get_freq('3Y') == 1000 + assert frequencies.get_freq('-1Y') == 1000 + assert frequencies.get_freq('W') == 4000 assert frequencies.get_freq('W-MON') == 4001 assert frequencies.get_freq('W-FRI') == 4005 @@ -369,6 +382,13 @@ def test_freq_group(self): assert frequencies.get_freq_group('-1A') == 1000 assert frequencies.get_freq_group('A-JAN') == 1000 assert frequencies.get_freq_group('A-MAY') == 1000 + + assert frequencies.get_freq_group('Y') == 1000 + assert frequencies.get_freq_group('3Y') == 1000 + assert frequencies.get_freq_group('-1Y') == 1000 + assert frequencies.get_freq_group('Y-JAN') == 1000 + assert frequencies.get_freq_group('Y-MAY') == 1000 + assert frequencies.get_freq_group(offsets.YearEnd()) == 1000 assert frequencies.get_freq_group(offsets.YearEnd(month=1)) == 1000 assert frequencies.get_freq_group(offsets.YearEnd(month=5)) == 1000 @@ -790,12 +810,6 @@ def test_series(self): for freq in [None, 'L']: s = Series(period_range('2013', periods=10, freq=freq)) pytest.raises(TypeError, lambda: frequencies.infer_freq(s)) - for freq in ['Y']: - - msg = frequencies._INVALID_FREQ_ERROR - with tm.assert_raises_regex(ValueError, msg): - s = Series(period_range('2013', periods=10, freq=freq)) - pytest.raises(TypeError, lambda: frequencies.infer_freq(s)) # DateTimeIndex for freq in ['M', 'L', 'S']: @@ -812,11 +826,12 @@ def test_legacy_offset_warnings(self): 'W@FRI', 'W@SAT', 'W@SUN', 'Q@JAN', 'Q@FEB', 'Q@MAR', 'A@JAN', 'A@FEB', 'A@MAR', 'A@APR', 'A@MAY', 'A@JUN', 'A@JUL', 'A@AUG', 'A@SEP', 'A@OCT', 'A@NOV', 'A@DEC', - 'WOM@1MON', 'WOM@2MON', 'WOM@3MON', 'WOM@4MON', - 'WOM@1TUE', 'WOM@2TUE', 'WOM@3TUE', 'WOM@4TUE', - 'WOM@1WED', 'WOM@2WED', 'WOM@3WED', 'WOM@4WED', - 'WOM@1THU', 'WOM@2THU', 'WOM@3THU', 'WOM@4THU' - 'WOM@1FRI', 'WOM@2FRI', 'WOM@3FRI', 'WOM@4FRI'] + 'Y@JAN', 'WOM@1MON', 'WOM@2MON', 'WOM@3MON', + 'WOM@4MON', 'WOM@1TUE', 'WOM@2TUE', 'WOM@3TUE', + 'WOM@4TUE', 'WOM@1WED', 'WOM@2WED', 'WOM@3WED', + 'WOM@4WED', 'WOM@1THU', 'WOM@2THU', 'WOM@3THU', + 'WOM@4THU', 'WOM@1FRI', 'WOM@2FRI', 'WOM@3FRI', + 'WOM@4FRI'] msg = frequencies._INVALID_FREQ_ERROR for freq in freqs: diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index c5f6c00a4005a..5c3c90520d1c3 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -422,6 +422,27 @@ def get_period_alias(offset_str): return _offset_to_period_map.get(offset_str, None) +_pure_alias = { + # 'A' is equivalent to 'Y'. + 'Y': 'A', + 'YS': 'AS', + 'BY': 'BA', + 'BYS': 'BAS', + 'Y-DEC': 'A-DEC', + 'Y-JAN': 'A-JAN', + 'Y-FEB': 'A-FEB', + 'Y-MAR': 'A-MAR', + 'Y-APR': 'A-APR', + 'Y-MAY': 'A-MAY', + 'Y-JUN': 'A-JUN', + 'Y-JUL': 'A-JUL', + 'Y-AUG': 'A-AUG', + 'Y-SEP': 'A-SEP', + 'Y-OCT': 'A-OCT', + 'Y-NOV': 'A-NOV', +} + + _lite_rule_alias = { 'W': 'W-SUN', 'Q': 'Q-DEC', @@ -718,6 +739,7 @@ def get_standard_freq(freq): def _period_str_to_code(freqstr): + freqstr = _pure_alias.get(freqstr, freqstr) freqstr = _lite_rule_alias.get(freqstr, freqstr) if freqstr not in _dont_uppercase: From e6ec2b81073ea0ac7fb6f04e667c2a14345e1a40 Mon Sep 17 00:00:00 2001 From: gfyoung Date: Sun, 16 Jul 2017 10:31:58 -0700 Subject: [PATCH 2/2] TST, MAINT: Add missing components to PR --- doc/source/timeseries.rst | 4 +- doc/source/whatsnew/v0.21.0.txt | 2 + .../indexes/datetimes/test_date_range.py | 25 ++++++++++ pandas/tseries/frequencies.py | 46 +++++++++---------- 4 files changed, 50 insertions(+), 27 deletions(-) diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index 1dd80aec4fd6c..8f02a86adbd48 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -1092,9 +1092,9 @@ frequencies. We will refer to these aliases as *offset aliases* "BQ", "business quarter endfrequency" "QS", "quarter start frequency" "BQS", "business quarter start frequency" - "A", "year end frequency" + "A, Y", "year end frequency" "BA", "business year end frequency" - "AS", "year start frequency" + "AS, YS", "year start frequency" "BAS", "business year start frequency" "BH", "business hour frequency" "H", "hourly frequency" diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index c63d4575bac43..b918cf42b3a40 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -40,6 +40,8 @@ Other Enhancements - :func:`DataFrame.clip()` and :func:`Series.clip()` have gained an ``inplace`` argument. (:issue:`15388`) - :func:`crosstab` has gained a ``margins_name`` parameter to define the name of the row / column that will contain the totals when ``margins=True``. (:issue:`15972`) - :func:`DataFrame.select_dtypes` now accepts scalar values for include/exclude as well as list-like. (:issue:`16855`) +- :func:`date_range` now accepts 'YS' in addition to 'AS' as an alias for start of year (:issue:`9313`) +- :func:`date_range` now accepts 'Y' in addition to 'A' as an alias for end of year (:issue:`9313`) .. _whatsnew_0210.api_breaking: diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py index 62686b356dc30..da4ca83c10dda 100644 --- a/pandas/tests/indexes/datetimes/test_date_range.py +++ b/pandas/tests/indexes/datetimes/test_date_range.py @@ -33,6 +33,31 @@ def test_date_range_gen_error(self): rng = date_range('1/1/2000 00:00', '1/1/2000 00:18', freq='5min') assert len(rng) == 4 + @pytest.mark.parametrize("freq", ["AS", "YS"]) + def test_begin_year_alias(self, freq): + # see gh-9313 + rng = date_range("1/1/2013", "7/1/2017", freq=freq) + exp = pd.DatetimeIndex(["2013-01-01", "2014-01-01", + "2015-01-01", "2016-01-01", + "2017-01-01"], freq=freq) + tm.assert_index_equal(rng, exp) + + @pytest.mark.parametrize("freq", ["A", "Y"]) + def test_end_year_alias(self, freq): + # see gh-9313 + rng = date_range("1/1/2013", "7/1/2017", freq=freq) + exp = pd.DatetimeIndex(["2013-12-31", "2014-12-31", + "2015-12-31", "2016-12-31"], freq=freq) + tm.assert_index_equal(rng, exp) + + @pytest.mark.parametrize("freq", ["BA", "BY"]) + def test_business_end_year_alias(self, freq): + # see gh-9313 + rng = date_range("1/1/2013", "7/1/2017", freq=freq) + exp = pd.DatetimeIndex(["2013-12-31", "2014-12-31", + "2015-12-31", "2016-12-30"], freq=freq) + tm.assert_index_equal(rng, exp) + def test_date_range_negative_freq(self): # GH 11018 rng = date_range('2011-12-31', freq='-2A', periods=3) diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index 5c3c90520d1c3..aa33a3849acb3 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -399,10 +399,14 @@ def _get_freq_str(base, mult=1): 'Q': 'Q', 'A': 'A', 'W': 'W', - 'M': 'M' + 'M': 'M', + 'Y': 'A', + 'BY': 'A', + 'YS': 'A', + 'BYS': 'A', } -need_suffix = ['QS', 'BQ', 'BQS', 'AS', 'BA', 'BAS'] +need_suffix = ['QS', 'BQ', 'BQS', 'YS', 'AS', 'BY', 'BA', 'BYS', 'BAS'] for __prefix in need_suffix: for _m in tslib._MONTHS: _offset_to_period_map['%s-%s' % (__prefix, _m)] = \ @@ -422,35 +426,18 @@ def get_period_alias(offset_str): return _offset_to_period_map.get(offset_str, None) -_pure_alias = { - # 'A' is equivalent to 'Y'. - 'Y': 'A', - 'YS': 'AS', - 'BY': 'BA', - 'BYS': 'BAS', - 'Y-DEC': 'A-DEC', - 'Y-JAN': 'A-JAN', - 'Y-FEB': 'A-FEB', - 'Y-MAR': 'A-MAR', - 'Y-APR': 'A-APR', - 'Y-MAY': 'A-MAY', - 'Y-JUN': 'A-JUN', - 'Y-JUL': 'A-JUL', - 'Y-AUG': 'A-AUG', - 'Y-SEP': 'A-SEP', - 'Y-OCT': 'A-OCT', - 'Y-NOV': 'A-NOV', -} - - _lite_rule_alias = { 'W': 'W-SUN', 'Q': 'Q-DEC', 'A': 'A-DEC', # YearEnd(month=12), + 'Y': 'A-DEC', 'AS': 'AS-JAN', # YearBegin(month=1), + 'YS': 'AS-JAN', 'BA': 'BA-DEC', # BYearEnd(month=12), + 'BY': 'BA-DEC', 'BAS': 'BAS-JAN', # BYearBegin(month=1), + 'BYS': 'BAS-JAN', 'Min': 'T', 'min': 'T', @@ -729,7 +716,17 @@ def get_standard_freq(freq): for _k, _v in compat.iteritems(_period_code_map): _reverse_period_code_map[_v] = _k -# Additional aliases +# Yearly aliases +year_aliases = {} + +for k, v in compat.iteritems(_period_code_map): + if k.startswith("A-"): + alias = "Y" + k[1:] + year_aliases[alias] = v + +_period_code_map.update(**year_aliases) +del year_aliases + _period_code_map.update({ "Q": 2000, # Quarterly - December year end (default quarterly) "A": 1000, # Annual @@ -739,7 +736,6 @@ def get_standard_freq(freq): def _period_str_to_code(freqstr): - freqstr = _pure_alias.get(freqstr, freqstr) freqstr = _lite_rule_alias.get(freqstr, freqstr) if freqstr not in _dont_uppercase: