Skip to content

Commit aead041

Browse files
authored
ENH: Use 'Y' as an alias for end of year (#16978)
Closes gh-9313 Redo of gh-16958
1 parent e5de21a commit aead041

File tree

5 files changed

+78
-18
lines changed

5 files changed

+78
-18
lines changed

Diff for: doc/source/timeseries.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -1092,9 +1092,9 @@ frequencies. We will refer to these aliases as *offset aliases*
10921092
"BQ", "business quarter endfrequency"
10931093
"QS", "quarter start frequency"
10941094
"BQS", "business quarter start frequency"
1095-
"A", "year end frequency"
1095+
"A, Y", "year end frequency"
10961096
"BA", "business year end frequency"
1097-
"AS", "year start frequency"
1097+
"AS, YS", "year start frequency"
10981098
"BAS", "business year start frequency"
10991099
"BH", "business hour frequency"
11001100
"H", "hourly frequency"

Diff for: doc/source/whatsnew/v0.21.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ Other Enhancements
7272
- :func:`DataFrame.clip()` and :func:`Series.clip()` have gained an ``inplace`` argument. (:issue:`15388`)
7373
- :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`)
7474
- :func:`DataFrame.select_dtypes` now accepts scalar values for include/exclude as well as list-like. (:issue:`16855`)
75+
- :func:`date_range` now accepts 'YS' in addition to 'AS' as an alias for start of year (:issue:`9313`)
76+
- :func:`date_range` now accepts 'Y' in addition to 'A' as an alias for end of year (:issue:`9313`)
7577

7678
.. _whatsnew_0210.api_breaking:
7779

Diff for: pandas/tests/indexes/datetimes/test_date_range.py

+25
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,31 @@ def test_date_range_gen_error(self):
3333
rng = date_range('1/1/2000 00:00', '1/1/2000 00:18', freq='5min')
3434
assert len(rng) == 4
3535

36+
@pytest.mark.parametrize("freq", ["AS", "YS"])
37+
def test_begin_year_alias(self, freq):
38+
# see gh-9313
39+
rng = date_range("1/1/2013", "7/1/2017", freq=freq)
40+
exp = pd.DatetimeIndex(["2013-01-01", "2014-01-01",
41+
"2015-01-01", "2016-01-01",
42+
"2017-01-01"], freq=freq)
43+
tm.assert_index_equal(rng, exp)
44+
45+
@pytest.mark.parametrize("freq", ["A", "Y"])
46+
def test_end_year_alias(self, freq):
47+
# see gh-9313
48+
rng = date_range("1/1/2013", "7/1/2017", freq=freq)
49+
exp = pd.DatetimeIndex(["2013-12-31", "2014-12-31",
50+
"2015-12-31", "2016-12-31"], freq=freq)
51+
tm.assert_index_equal(rng, exp)
52+
53+
@pytest.mark.parametrize("freq", ["BA", "BY"])
54+
def test_business_end_year_alias(self, freq):
55+
# see gh-9313
56+
rng = date_range("1/1/2013", "7/1/2017", freq=freq)
57+
exp = pd.DatetimeIndex(["2013-12-31", "2014-12-31",
58+
"2015-12-31", "2016-12-30"], freq=freq)
59+
tm.assert_index_equal(rng, exp)
60+
3661
def test_date_range_negative_freq(self):
3762
# GH 11018
3863
rng = date_range('2011-12-31', freq='-2A', periods=3)

Diff for: pandas/tests/tseries/test_frequencies.py

+28-13
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,10 @@ def test_anchored_shortcuts(self):
248248

249249
# ensure invalid cases fail as expected
250250
invalid_anchors = ['SM-0', 'SM-28', 'SM-29',
251-
'SM-FOO', 'BSM', 'SM--1'
251+
'SM-FOO', 'BSM', 'SM--1',
252252
'SMS-1', 'SMS-28', 'SMS-30',
253-
'SMS-BAR', 'BSMS', 'SMS--2']
253+
'SMS-BAR', 'SMS-BYR' 'BSMS',
254+
'SMS--2']
254255
for invalid_anchor in invalid_anchors:
255256
with tm.assert_raises_regex(ValueError,
256257
'Invalid frequency: '):
@@ -292,11 +293,15 @@ def test_get_rule_month():
292293

293294
result = frequencies._get_rule_month('A-DEC')
294295
assert (result == 'DEC')
296+
result = frequencies._get_rule_month('Y-DEC')
297+
assert (result == 'DEC')
295298
result = frequencies._get_rule_month(offsets.YearEnd())
296299
assert (result == 'DEC')
297300

298301
result = frequencies._get_rule_month('A-MAY')
299302
assert (result == 'MAY')
303+
result = frequencies._get_rule_month('Y-MAY')
304+
assert (result == 'MAY')
300305
result = frequencies._get_rule_month(offsets.YearEnd(month=5))
301306
assert (result == 'MAY')
302307

@@ -305,6 +310,10 @@ def test_period_str_to_code():
305310
assert (frequencies._period_str_to_code('A') == 1000)
306311
assert (frequencies._period_str_to_code('A-DEC') == 1000)
307312
assert (frequencies._period_str_to_code('A-JAN') == 1001)
313+
assert (frequencies._period_str_to_code('Y') == 1000)
314+
assert (frequencies._period_str_to_code('Y-DEC') == 1000)
315+
assert (frequencies._period_str_to_code('Y-JAN') == 1001)
316+
308317
assert (frequencies._period_str_to_code('Q') == 2000)
309318
assert (frequencies._period_str_to_code('Q-DEC') == 2000)
310319
assert (frequencies._period_str_to_code('Q-FEB') == 2002)
@@ -349,6 +358,10 @@ def test_freq_code(self):
349358
assert frequencies.get_freq('3A') == 1000
350359
assert frequencies.get_freq('-1A') == 1000
351360

361+
assert frequencies.get_freq('Y') == 1000
362+
assert frequencies.get_freq('3Y') == 1000
363+
assert frequencies.get_freq('-1Y') == 1000
364+
352365
assert frequencies.get_freq('W') == 4000
353366
assert frequencies.get_freq('W-MON') == 4001
354367
assert frequencies.get_freq('W-FRI') == 4005
@@ -369,6 +382,13 @@ def test_freq_group(self):
369382
assert frequencies.get_freq_group('-1A') == 1000
370383
assert frequencies.get_freq_group('A-JAN') == 1000
371384
assert frequencies.get_freq_group('A-MAY') == 1000
385+
386+
assert frequencies.get_freq_group('Y') == 1000
387+
assert frequencies.get_freq_group('3Y') == 1000
388+
assert frequencies.get_freq_group('-1Y') == 1000
389+
assert frequencies.get_freq_group('Y-JAN') == 1000
390+
assert frequencies.get_freq_group('Y-MAY') == 1000
391+
372392
assert frequencies.get_freq_group(offsets.YearEnd()) == 1000
373393
assert frequencies.get_freq_group(offsets.YearEnd(month=1)) == 1000
374394
assert frequencies.get_freq_group(offsets.YearEnd(month=5)) == 1000
@@ -790,12 +810,6 @@ def test_series(self):
790810
for freq in [None, 'L']:
791811
s = Series(period_range('2013', periods=10, freq=freq))
792812
pytest.raises(TypeError, lambda: frequencies.infer_freq(s))
793-
for freq in ['Y']:
794-
795-
msg = frequencies._INVALID_FREQ_ERROR
796-
with tm.assert_raises_regex(ValueError, msg):
797-
s = Series(period_range('2013', periods=10, freq=freq))
798-
pytest.raises(TypeError, lambda: frequencies.infer_freq(s))
799813

800814
# DateTimeIndex
801815
for freq in ['M', 'L', 'S']:
@@ -812,11 +826,12 @@ def test_legacy_offset_warnings(self):
812826
'W@FRI', 'W@SAT', 'W@SUN', 'Q@JAN', 'Q@FEB', 'Q@MAR',
813827
'A@JAN', 'A@FEB', 'A@MAR', 'A@APR', 'A@MAY', 'A@JUN',
814828
'A@JUL', 'A@AUG', 'A@SEP', 'A@OCT', 'A@NOV', 'A@DEC',
815-
'WOM@1MON', 'WOM@2MON', 'WOM@3MON', 'WOM@4MON',
816-
'WOM@1TUE', 'WOM@2TUE', 'WOM@3TUE', 'WOM@4TUE',
817-
'WOM@1WED', 'WOM@2WED', 'WOM@3WED', 'WOM@4WED',
818-
'WOM@1THU', 'WOM@2THU', 'WOM@3THU', 'WOM@4THU'
819-
'WOM@1FRI', 'WOM@2FRI', 'WOM@3FRI', 'WOM@4FRI']
829+
'Y@JAN', 'WOM@1MON', 'WOM@2MON', 'WOM@3MON',
830+
'WOM@4MON', 'WOM@1TUE', 'WOM@2TUE', 'WOM@3TUE',
831+
'WOM@4TUE', 'WOM@1WED', 'WOM@2WED', 'WOM@3WED',
832+
'WOM@4WED', 'WOM@1THU', 'WOM@2THU', 'WOM@3THU',
833+
'WOM@4THU', 'WOM@1FRI', 'WOM@2FRI', 'WOM@3FRI',
834+
'WOM@4FRI']
820835

821836
msg = frequencies._INVALID_FREQ_ERROR
822837
for freq in freqs:

Diff for: pandas/tseries/frequencies.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -399,10 +399,14 @@ def _get_freq_str(base, mult=1):
399399
'Q': 'Q',
400400
'A': 'A',
401401
'W': 'W',
402-
'M': 'M'
402+
'M': 'M',
403+
'Y': 'A',
404+
'BY': 'A',
405+
'YS': 'A',
406+
'BYS': 'A',
403407
}
404408

405-
need_suffix = ['QS', 'BQ', 'BQS', 'AS', 'BA', 'BAS']
409+
need_suffix = ['QS', 'BQ', 'BQS', 'YS', 'AS', 'BY', 'BA', 'BYS', 'BAS']
406410
for __prefix in need_suffix:
407411
for _m in tslib._MONTHS:
408412
_offset_to_period_map['%s-%s' % (__prefix, _m)] = \
@@ -427,9 +431,13 @@ def get_period_alias(offset_str):
427431
'Q': 'Q-DEC',
428432

429433
'A': 'A-DEC', # YearEnd(month=12),
434+
'Y': 'A-DEC',
430435
'AS': 'AS-JAN', # YearBegin(month=1),
436+
'YS': 'AS-JAN',
431437
'BA': 'BA-DEC', # BYearEnd(month=12),
438+
'BY': 'BA-DEC',
432439
'BAS': 'BAS-JAN', # BYearBegin(month=1),
440+
'BYS': 'BAS-JAN',
433441

434442
'Min': 'T',
435443
'min': 'T',
@@ -708,7 +716,17 @@ def get_standard_freq(freq):
708716
for _k, _v in compat.iteritems(_period_code_map):
709717
_reverse_period_code_map[_v] = _k
710718

711-
# Additional aliases
719+
# Yearly aliases
720+
year_aliases = {}
721+
722+
for k, v in compat.iteritems(_period_code_map):
723+
if k.startswith("A-"):
724+
alias = "Y" + k[1:]
725+
year_aliases[alias] = v
726+
727+
_period_code_map.update(**year_aliases)
728+
del year_aliases
729+
712730
_period_code_map.update({
713731
"Q": 2000, # Quarterly - December year end (default quarterly)
714732
"A": 1000, # Annual

0 commit comments

Comments
 (0)