Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Use 'Y' as an alias for end of year #16978

Merged
merged 2 commits into from
Jul 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions doc/source/timeseries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions doc/source/whatsnew/v0.21.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
25 changes: 25 additions & 0 deletions pandas/tests/indexes/datetimes/test_date_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to the class for Business tests (below)

Copy link
Member Author

@gfyoung gfyoung Jul 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this is for date_range, not bdate_range though. There are a bunch of business tests related to business and date_range already in this class (and not the Business one).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm ok.

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)
Expand Down
41 changes: 28 additions & 13 deletions pandas/tests/tseries/test_frequencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: '):
Expand Down Expand Up @@ -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')

Expand All @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests for YS as well (like AS)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Umm...where do we actually test 'AS' in this file?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added tests for "AS" in test_date_range.

assert (frequencies._period_str_to_code('Y') == 1000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add tests that validate things with 'A', 'YS', 'AS' as well here, similar to Y

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean here: neither YS nor AS are considered valid "period strings"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, where is AS tested then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In date_range at the very least.

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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also BYS (assume BAS is validated somewhere)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean here: neither BYS nor BAS are considered valid "frequency groups"

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
Expand Down Expand Up @@ -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']:
Expand All @@ -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:
Expand Down
24 changes: 21 additions & 3 deletions pandas/tseries/frequencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)] = \
Expand All @@ -427,9 +431,13 @@ def get_period_alias(offset_str):
'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',
Expand Down Expand Up @@ -708,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
Expand Down