Skip to content

Commit 41c63ab

Browse files
authored
DOC/ENH: Holiday exclusion argument (#61600)
* added exclude_dates argument and test for holiday class * added entry on whatsnew * moved pull request to v3.0.0 whatsnew *  updated exclude_dates to be [Timestamp] in constructor * updated Holiday.dates() to use difference method * condensed type_checking import statement to 1 line * switchted exclude_dates type from list[Timestamp] to DatetimeIndex * raise ValueError if exclude_dates is not DatetimeIndex * isinstance to check exclude_data * updated tests for DatetimeIndex type change * added proper result-expected assertion pattern for tests * removed redundant asserts
1 parent 2bf3dc9 commit 41c63ab

File tree

3 files changed

+111
-0
lines changed

3 files changed

+111
-0
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Other enhancements
6464
- :meth:`Series.nlargest` uses a 'stable' sort internally and will preserve original ordering.
6565
- :class:`ArrowDtype` now supports ``pyarrow.JsonType`` (:issue:`60958`)
6666
- :class:`DataFrameGroupBy` and :class:`SeriesGroupBy` methods ``sum``, ``mean``, ``median``, ``prod``, ``min``, ``max``, ``std``, ``var`` and ``sem`` now accept ``skipna`` parameter (:issue:`15675`)
67+
- :class:`Holiday` has gained the constructor argument and field ``exclude_dates`` to exclude specific datetimes from a custom holiday calendar (:issue:`54382`)
6768
- :class:`Rolling` and :class:`Expanding` now support ``nunique`` (:issue:`26958`)
6869
- :class:`Rolling` and :class:`Expanding` now support aggregations ``first`` and ``last`` (:issue:`33155`)
6970
- :func:`read_parquet` accepts ``to_pandas_kwargs`` which are forwarded to :meth:`pyarrow.Table.to_pandas` which enables passing additional keywords to customize the conversion to pandas, such as ``maps_as_pydicts`` to read the Parquet map data type as python dictionaries (:issue:`56842`)

pandas/tests/tseries/holiday/test_holiday.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,104 @@ def test_holidays_with_timezone_specified_but_no_occurences():
353353
expected_results.index = expected_results.index.as_unit("ns")
354354

355355
tm.assert_equal(test_case, expected_results)
356+
357+
358+
def test_holiday_with_exclusion():
359+
# GH 54382
360+
start = Timestamp("2020-05-01")
361+
end = Timestamp("2025-05-31")
362+
exclude = DatetimeIndex([Timestamp("2022-05-30")]) # Queen's platinum Jubilee
363+
364+
queens_jubilee_uk_spring_bank_holiday: Holiday = Holiday(
365+
"Queen's Jubilee UK Spring Bank Holiday",
366+
month=5,
367+
day=31,
368+
offset=DateOffset(weekday=MO(-1)),
369+
exclude_dates=exclude,
370+
)
371+
372+
result = queens_jubilee_uk_spring_bank_holiday.dates(start, end)
373+
expected = DatetimeIndex(
374+
[
375+
Timestamp("2020-05-25"),
376+
Timestamp("2021-05-31"),
377+
Timestamp("2023-05-29"),
378+
Timestamp("2024-05-27"),
379+
Timestamp("2025-05-26"),
380+
],
381+
dtype="datetime64[ns]",
382+
)
383+
tm.assert_index_equal(result, expected)
384+
385+
386+
def test_holiday_with_multiple_exclusions():
387+
start = Timestamp("2025-01-01")
388+
end = Timestamp("2065-12-31")
389+
exclude = DatetimeIndex(
390+
[
391+
Timestamp("2025-01-01"),
392+
Timestamp("2042-01-01"),
393+
Timestamp("2061-01-01"),
394+
]
395+
) # Yakudoshi new year
396+
397+
yakudoshi_new_year: Holiday = Holiday(
398+
"Yakudoshi New Year", month=1, day=1, exclude_dates=exclude
399+
)
400+
401+
result = yakudoshi_new_year.dates(start, end)
402+
expected = DatetimeIndex(
403+
[
404+
Timestamp("2026-01-01"),
405+
Timestamp("2027-01-01"),
406+
Timestamp("2028-01-01"),
407+
Timestamp("2029-01-01"),
408+
Timestamp("2030-01-01"),
409+
Timestamp("2031-01-01"),
410+
Timestamp("2032-01-01"),
411+
Timestamp("2033-01-01"),
412+
Timestamp("2034-01-01"),
413+
Timestamp("2035-01-01"),
414+
Timestamp("2036-01-01"),
415+
Timestamp("2037-01-01"),
416+
Timestamp("2038-01-01"),
417+
Timestamp("2039-01-01"),
418+
Timestamp("2040-01-01"),
419+
Timestamp("2041-01-01"),
420+
Timestamp("2043-01-01"),
421+
Timestamp("2044-01-01"),
422+
Timestamp("2045-01-01"),
423+
Timestamp("2046-01-01"),
424+
Timestamp("2047-01-01"),
425+
Timestamp("2048-01-01"),
426+
Timestamp("2049-01-01"),
427+
Timestamp("2050-01-01"),
428+
Timestamp("2051-01-01"),
429+
Timestamp("2052-01-01"),
430+
Timestamp("2053-01-01"),
431+
Timestamp("2054-01-01"),
432+
Timestamp("2055-01-01"),
433+
Timestamp("2056-01-01"),
434+
Timestamp("2057-01-01"),
435+
Timestamp("2058-01-01"),
436+
Timestamp("2059-01-01"),
437+
Timestamp("2060-01-01"),
438+
Timestamp("2062-01-01"),
439+
Timestamp("2063-01-01"),
440+
Timestamp("2064-01-01"),
441+
Timestamp("2065-01-01"),
442+
],
443+
dtype="datetime64[ns]",
444+
)
445+
tm.assert_index_equal(result, expected)
446+
447+
448+
def test_exclude_date_value_error():
449+
msg = "exclude_dates must be None or of type DatetimeIndex."
450+
451+
with pytest.raises(ValueError, match=msg):
452+
exclude = [
453+
Timestamp("2025-06-10"),
454+
Timestamp("2026-06-10"),
455+
]
456+
Holiday("National Ice Tea Day", month=6, day=10, exclude_dates=exclude)

pandas/tseries/holiday.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def __init__(
169169
start_date=None,
170170
end_date=None,
171171
days_of_week: tuple | None = None,
172+
exclude_dates: DatetimeIndex | None = None,
172173
) -> None:
173174
"""
174175
Parameters
@@ -193,6 +194,8 @@ class from pandas.tseries.offsets, default None
193194
days_of_week : tuple of int or dateutil.relativedelta weekday strs, default None
194195
Provide a tuple of days e.g (0,1,2,3,) for Monday Through Thursday
195196
Monday=0,..,Sunday=6
197+
exclude_dates : DatetimeIndex or default None
198+
Specific dates to exclude e.g. skipping a specific year's holiday
196199
197200
Examples
198201
--------
@@ -257,6 +260,9 @@ class from pandas.tseries.offsets, default None
257260
self.observance = observance
258261
assert days_of_week is None or type(days_of_week) == tuple
259262
self.days_of_week = days_of_week
263+
if not (exclude_dates is None or isinstance(exclude_dates, DatetimeIndex)):
264+
raise ValueError("exclude_dates must be None or of type DatetimeIndex.")
265+
self.exclude_dates = exclude_dates
260266

261267
def __repr__(self) -> str:
262268
info = ""
@@ -328,6 +334,9 @@ def dates(
328334
holiday_dates = holiday_dates[
329335
(holiday_dates >= filter_start_date) & (holiday_dates <= filter_end_date)
330336
]
337+
338+
if self.exclude_dates is not None:
339+
holiday_dates = holiday_dates.difference(self.exclude_dates)
331340
if return_name:
332341
return Series(self.name, index=holiday_dates)
333342
return holiday_dates

0 commit comments

Comments
 (0)