From e7ad7ae6a792eced0c6bddf81e0a4bef379c999d Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Mon, 27 Feb 2023 16:11:08 -0800 Subject: [PATCH 1/2] Update python-holidays integration. Bump python-holidays version to 0.20. Migrate to python-holidays as a single source of country holidays. --- neuralprophet/hdays.py | 601 ----------------------------------- neuralprophet/hdays_utils.py | 22 +- requirements/base.txt | 2 +- tests/test_hdays_utils.py | 18 ++ tests/test_utils.py | 7 - 5 files changed, 29 insertions(+), 621 deletions(-) delete mode 100644 neuralprophet/hdays.py create mode 100644 tests/test_hdays_utils.py diff --git a/neuralprophet/hdays.py b/neuralprophet/hdays.py deleted file mode 100644 index b11e5e624..000000000 --- a/neuralprophet/hdays.py +++ /dev/null @@ -1,601 +0,0 @@ -from __future__ import absolute_import, division, print_function - -import logging -from datetime import date, timedelta - -from convertdate.islamic import from_gregorian, to_gregorian -from dateutil.easter import EASTER_ORTHODOX, easter -from dateutil.relativedelta import relativedelta as rd -from holidays import HolidayBase, Turkey -from holidays.constants import WEEKEND -from lunarcalendar import Converter, Lunar - -log = logging.getLogger("NP.hdays") - - -# Official public holidays at a country level -# ------------ Holidays in Indonesia--------------------- -class Indonesia(HolidayBase): - """ - Implement public holidays in Indonesia - - Reference: - https://en.wikipedia.org/wiki/Public_holidays_in_Indonesia - - Please note: Indonesia is a multi-cultural community and we only implement - the national wide public holidays. - """ - - def __init__(self, **kwargs): - self.country = "ID" - HolidayBase.__init__(self, **kwargs) - - def _populate(self, year): - # New Year's Day - if not self.observed and date(year, 1, 1).weekday() in WEEKEND: - pass - else: - self[date(year, 1, 1)] = "New Year's Day" - - # Chinese New Year/ Spring Festival - name = "Chinese New Year" - for offset in range(-1, 2, 1): - ds = Converter.Lunar2Solar(Lunar(year + offset, 1, 1)).to_date() - if ds.year == year: - self[ds] = name - - # Day of Silence / Nyepi - # Note: - # This holiday is determined by Balinese calendar, which is not currently - # available. Only hard coded version of this holiday from 2009 to 2019 - # is available. - warning_msg = "We only support Nyepi holiday from 2009 to 2019" - log.warning(warning_msg) - - name = "Day of Silence/ Nyepi" - if year == 2009: - self[date(year, 3, 26)] = name - elif year == 2010: - self[date(year, 3, 16)] = name - elif year == 2011: - self[date(year, 3, 5)] = name - elif year == 2012: - self[date(year, 3, 23)] = name - elif year == 2013: - self[date(year, 3, 12)] = name - elif year == 2014: - self[date(year, 3, 31)] = name - elif year == 2015: - self[date(year, 3, 21)] = name - elif year == 2016: - self[date(year, 3, 9)] = name - elif year == 2017: - self[date(year, 3, 28)] = name - elif year == 2018: - self[date(year, 3, 17)] = name - elif year == 2019: - self[date(year, 3, 7)] = name - else: - pass - - # Ascension of the Prophet - name = "Ascension of the Prophet" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 3, 17)[0] - y, m, d = to_gregorian(islam_year, 7, 27) - if y == year: - self[date(y, m, d)] = name - - # Labor Day - name = "Labor Day" - self[date(year, 5, 1)] = name - - # Ascension of Jesus Christ - name = "Ascension of Jesus" - for offset in range(-1, 2, 1): - ds = easter(year + offset) + rd(days=+39) - if ds.year == year: - self[ds] = name - - # Buddha's Birthday - name = "Buddha's Birthday" - for offset in range(-1, 2, 1): - ds = Converter.Lunar2Solar(Lunar(year + offset, 4, 15)).to_date() - if ds.year == year: - self[ds] = name - - # Pancasila Day, since 2017 - if year >= 2017: - name = "Pancasila Day" - self[date(year, 6, 1)] = name - - # Eid al-Fitr - name = "Eid al-Fitr" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 6, 15)[0] - y1, m1, d1 = to_gregorian(islam_year, 10, 1) - y2, m2, d2 = to_gregorian(islam_year, 10, 2) - if y1 == year: - self[date(y1, m2, d2)] = name - if y2 == year: - self[date(y2, m2, d2)] = name - - # Independence Day - name = "Independence Day" - self[date(year, 8, 17)] = name - - # Feast of the Sacrifice - name = "Feast of the Sacrifice" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 8, 22)[0] - y, m, d = to_gregorian(islam_year, 12, 10) - if y == year: - self[date(y, m, d)] = name - - # Islamic New Year - name = "Islamic New Year" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 9, 11)[0] - y, m, d = to_gregorian(islam_year + 1, 1, 1) - if y == year: - self[date(y, m, d)] = name - - # Birth of the Prophet - name = "Birth of the Prophet" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 11, 20)[0] - y, m, d = to_gregorian(islam_year + 1, 3, 12) - if y == year: - self[date(y, m, d)] = name - - # Christmas - self[date(year, 12, 25)] = "Christmas" - - -class ID(Indonesia): - pass - - -# ------------ Holidays in Thailand--------------------- -class Thailand(HolidayBase): - """ - Implement public holidays in Thailand - - Reference: - https://en.wikipedia.org/wiki/Public_holidays_in_Thailand - """ - - def __init__(self, **kwargs): - self.country = "TH" - HolidayBase.__init__(self, **kwargs) - - def _populate(self, year): - # New Year's Day - name = "New Year's Day" - self[date(year, 1, 1)] = name - - # Magha Pujab - # Note: - # This holiday is determined by Buddhist calendar, which is not currently - # available. Only hard coded version of this holiday from 2016 to 2019 - # is available. - - name = "Magha Pujab/Makha Bucha" - if year == 2016: - self[date(year, 2, 22)] = name - elif year == 2017: - self[date(year, 2, 11)] = name - elif year == 2018: - self[date(year, 3, 1)] = name - elif year == 2019: - self[date(year, 2, 19)] = name - else: - pass - - # Chakri Memorial Day - name = "Chakri Memorial Day" - april_6 = date(year, 4, 6).weekday() - if april_6 == 5: - self[date(year, 4, 6 + 2)] = name - elif april_6 == 6: - self[date(year, 4, 6 + 1)] = name - else: - self[date(year, 4, 6)] = name - - # Songkran Festival - name = "Songkran Festival" - self[date(year, 4, 14)] = name - - # Royal Ploughing Ceremony - # arbitrary day in May - - # Buddha's Birthday - name = "Buddha's Birthday" - for offset in range(-1, 2, 1): - ds = Converter.Lunar2Solar(Lunar(year + offset, 4, 15)).to_date() - if ds.year == year: - self[ds] = name - - # Coronation Day, removed in 2017 - name = "Coronation Day" - if year < 2017: - self[date(year, 5, 5)] = name - - # King Maha Vajiralongkorn's Birthday - name = "King Maha Vajiralongkorn's Birthday" - self[date(year, 7, 28)] = name - - # Asalha Puja - # This is also a Buddha holiday, and we only implement - # the hard coded version from 2006 to 2025 - # reference: - # http://www.when-is.com/asalha_puja.asp - warning_msg = "We only support Asalha Puja holiday from 2006 to 2025" - log.warning(warning_msg) - name = "Asalha Puja" - if year == 2006: - self[date(year, 7, 11)] = name - elif year == 2007: - self[date(year, 6, 30)] = name - elif year == 2008: - self[date(year, 7, 18)] = name - elif year == 2009: - self[date(year, 7, 7)] = name - elif year == 2010: - self[date(year, 7, 25)] = name - elif year == 2011: - self[date(year, 7, 15)] = name - elif year == 2012: - self[date(year, 8, 2)] = name - elif year == 2013: - self[date(year, 7, 30)] = name - elif year == 2014: - self[date(year, 7, 13)] = name - elif year == 2015: - self[date(year, 7, 30)] = name - elif year == 2016: - self[date(year, 7, 15)] = name - elif year == 2017: - self[date(year, 7, 9)] = name - elif year == 2018: - self[date(year, 7, 29)] = name - elif year == 2019: - self[date(year, 7, 16)] = name - elif year == 2020: - self[date(year, 7, 5)] = name - elif year == 2021: - self[date(year, 7, 24)] = name - elif year == 2022: - self[date(year, 7, 13)] = name - elif year == 2023: - self[date(year, 7, 3)] = name - elif year == 2024: - self[date(year, 7, 21)] = name - elif year == 2025: - self[date(year, 7, 10)] = name - else: - pass - - # Beginning of Vassa - warning_msg = "We only support Vassa holiday from 2006 to 2020" - log.warning(warning_msg) - name = "Beginning of Vassa" - if year == 2006: - self[date(year, 7, 12)] = name - elif year == 2007: - self[date(year, 7, 31)] = name - elif year == 2008: - self[date(year, 7, 19)] = name - elif year == 2009: - self[date(year, 7, 8)] = name - elif year == 2010: - self[date(year, 7, 27)] = name - elif year == 2011: - self[date(year, 7, 16)] = name - elif year == 2012: - self[date(year, 8, 3)] = name - elif year == 2013: - self[date(year, 7, 23)] = name - elif year == 2014: - self[date(year, 7, 13)] = name - elif year == 2015: - self[date(year, 8, 1)] = name - elif year == 2016: - self[date(year, 7, 20)] = name - elif year == 2017: - self[date(year, 7, 9)] = name - elif year == 2018: - self[date(year, 7, 28)] = name - elif year == 2019: - self[date(year, 7, 17)] = name - elif year == 2020: - self[date(year, 7, 6)] = name - else: - pass - - # The Queen Sirikit's Birthday - name = "The Queen Sirikit's Birthday" - self[date(year, 8, 12)] = name - - # Anniversary for the Death of King Bhumibol Adulyadej - name = "Anniversary for the Death of King Bhumibol Adulyadej" - self[date(year, 10, 13)] = name - - # King Chulalongkorn Day - name = "King Chulalongkorn Day" - self[date(year, 10, 23)] = name - - # King Bhumibol Adulyadej's Birthday Anniversary - name = "King Bhumibol Adulyadej's Birthday Anniversary" - self[date(year, 12, 5)] = name - - # Constitution Day - name = "Constitution Day" - self[date(year, 12, 10)] = name - - # New Year's Eve - name = "New Year's Eve" - self[date(year, 12, 31)] = name - - -class TH(Thailand): - pass - - -# ------------ Holidays in Philippines--------------------- -class Philippines(HolidayBase): - """ - Implement public holidays in Philippines - - Reference: - https://en.wikipedia.org/wiki/Public_holidays_in_Thailand - """ - - def __init__(self, **kwargs): - self.country = "PH" - HolidayBase.__init__(self, **kwargs) - - def _populate(self, year): - # New Year's Day - name = "New Year's Day" - self[date(year, 1, 1)] = name - - # Maundy Thursday - name = "Maundy Thursday" - for offset in range(-1, 2, 1): - ds = easter(year + offset) - rd(days=3) - if ds.year == year: - self[ds] = name - - # Good Friday - name = "Good Friday" - for offset in range(-1, 2, 1): - ds = easter(year + offset) - rd(days=2) - if ds.year == year: - self[ds] = name - - # Day of Valor - name = "Day of Valor" - self[date(year, 4, 9)] = name - - # Labor Day - name = "Labor Day" - self[date(year, 5, 1)] = name - - # Independence Day - name = "Independence Day" - self[date(year, 6, 12)] = name - - # Eid al-Fitr - name = "Eid al-Fitr" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 6, 15)[0] - y, m, d = to_gregorian(islam_year, 10, 1) - ds = date(y, m, d) - timedelta(days=1) - if ds.year == year: - self[ds] = name - - # Eid al-Adha, i.e., Feast of the Sacrifice - name = "Feast of the Sacrifice" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 8, 22)[0] - y, m, d = to_gregorian(islam_year, 12, 10) - if y == year: - self[date(y, m, d)] = name - - # National Heroes' Day - name = "National Heroes' Day" - self[date(year, 8, 27)] = name - - # Bonifacio Day - name = "Bonifacio Day" - self[date(year, 11, 30)] = name - - # Christmas Day - name = "Christmas Day" - self[date(year, 12, 25)] = name - - # Rizal Day - name = "Rizal Day" - self[date(year, 12, 30)] = name - - -class PH(Philippines): - pass - - -# ------------ Holidays in Turkey--------------------- -# This is now in Holidays, but with alias TR instead of the TU that we used. -# Include TU as an alias for backwards compatibility. - - -class TU(Turkey): - pass - - -# ------------ Holidays in Pakistan--------------------- -class Pakistan(HolidayBase): - """ - Implement public holidays in Pakistan - - Reference: - https://en.wikipedia.org/wiki/Public_holidays_in_Pakistan - """ - - def __init__(self, **kwargs): - self.country = "PK" - HolidayBase.__init__(self, **kwargs) - - def _populate(self, year): - # Kashmir Solidarity Day - name = "Kashmir Solidarity Day" - self[date(year, 2, 5)] = name - - # Pakistan Day - name = "Pakistan Day" - self[date(year, 3, 23)] = name - - # Labor Day - name = "Labor Day" - self[date(year, 5, 1)] = name - - # Independence Day - name = "Independence Day" - self[date(year, 8, 14)] = name - - # Iqbal Day - name = "Iqbal Day" - self[date(year, 11, 9)] = name - - # Christmas Day - # Also birthday of PK founder - name = "Christmas Day" - self[date(year, 12, 25)] = name - - # Eid al-Adha, i.e., Feast of the Sacrifice - name = "Feast of the Sacrifice" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 8, 22)[0] - y1, m1, d1 = to_gregorian(islam_year, 12, 10) - y2, m2, d2 = to_gregorian(islam_year, 12, 11) - y3, m3, d3 = to_gregorian(islam_year, 12, 12) - if y1 == year: - self[date(y1, m1, d1)] = name - if y2 == year: - self[date(y2, m2, d2)] = name - if y3 == year: - self[date(y3, m3, d3)] = name - - # Eid al-Fitr - name = "Eid al-Fitr" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 6, 15)[0] - y1, m1, d1 = to_gregorian(islam_year, 10, 1) - y2, m2, d2 = to_gregorian(islam_year, 10, 2) - y3, m3, d3 = to_gregorian(islam_year, 10, 3) - if y1 == year: - self[date(y1, m1, d1)] = name - if y2 == year: - self[date(y2, m2, d2)] = name - if y3 == year: - self[date(y3, m3, d3)] = name - - # Mawlid, Birth of the Prophet - # 12th day of 3rd Islamic month - name = "Mawlid" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 11, 20)[0] - y, m, d = to_gregorian(islam_year, 3, 12) - if y == year: - self[date(y, m, d)] = name - - # Day of Ashura - # 10th and 11th days of 1st Islamic month - name = "Day of Ashura" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 10, 1)[0] - y1, m1, d1 = to_gregorian(islam_year, 1, 10) - y2, m2, d2 = to_gregorian(islam_year, 1, 11) - if y1 == year: - self[date(y1, m1, d1)] = name - if y2 == year: - self[date(y2, m2, d2)] = name - - # Shab e Mairaj - name = "Shab e Mairaj" - for offset in range(-1, 2, 1): - islam_year = from_gregorian(year + offset, 4, 13)[0] - y, m, d = to_gregorian(islam_year, 7, 27) - if y == year: - self[date(y, m, d)] = name - - # Defence Day - name = "Defence Day" - self[date(year, 9, 6)] = name - - # Death Anniversary of Quaid-e-Azam - name = "Death Anniversary of Quaid-e-Azam" - self[date(year, 9, 11)] = name - - -class PK(Pakistan): - pass - - -# ------------ Holidays in Belarus--------------------- -class Belarus(HolidayBase): - """ - Implement public holidays in Belarus - - Reference: - https://en.wikipedia.org/wiki/Public_holidays_in_Belarus - - Please note: - Some holidays might collide with weekends and therefore not compensated with next business day - as International Women's Day - """ - - def __init__(self, **kwargs): - self.country = "BY" - HolidayBase.__init__(self, **kwargs) - - def _populate(self, year): - # New Year's Day - name = "New Year's Day" - self[date(year, 1, 1)] = name - - # Orthodox Christmas day - name = "Orthodox Christmas Day" - self[date(year, 1, 7)] = name - - # International Women's Day - name = "International Women's Day" - self[date(year, 3, 8)] = name - - # Commemoration Day - name = "Commemoration Day" - self[easter(year, EASTER_ORTHODOX) + timedelta(days=9)] = name - - # Spring and Labour Day - name = "Spring and Labour Day" - self[date(year, 5, 1)] = name - - # Victory Day - name = "Victory Day" - self[date(year, 5, 9)] = name - - # Independence Day - name = "Independence Day" - self[date(year, 7, 3)] = name - - # October Revolution Day - name = "October Revolution Day" - self[date(year, 11, 7)] = name - - # Dec. 25 Christmas Day - name = "Christmas Day" - self[date(year, 12, 25)] = name - - -class BY(Belarus): - pass diff --git a/neuralprophet/hdays_utils.py b/neuralprophet/hdays_utils.py index d0a80fdd0..f827b9237 100644 --- a/neuralprophet/hdays_utils.py +++ b/neuralprophet/hdays_utils.py @@ -1,8 +1,6 @@ from typing import Iterable, Optional, Union -import holidays as pyholidays - -from neuralprophet import hdays as hdays_part2 +import holidays def get_country_holidays(country: str, years: Optional[Union[int, Iterable[int]]] = None): @@ -22,12 +20,12 @@ def get_country_holidays(country: str, years: Optional[Union[int, Iterable[int]] All possible holiday dates and names of given country """ - try: - holidays_country = getattr(hdays_part2, country)(years=years) - except AttributeError: - try: - holidays_country = getattr(pyholidays, country)(years=years) - except AttributeError: - raise AttributeError(f"Holidays in {country} are not currently supported!") - - return holidays_country + substitutions = { + "TU": "TR", # For compatibility with Turkey as "TU" cases. + } + + country = substitutions.get(country, country) + if not hasattr(holidays, country): + raise AttributeError(f"Holidays in {country} are not currently supported!") + + return getattr(holidays, country)(years=years) diff --git a/requirements/base.txt b/requirements/base.txt index 2da52f197..b2846f299 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,7 +1,7 @@ captum>=0.5.0 convertdate>=2.1.2 dataclasses>=0.6;python_version<'3.7' -holidays>=0.11.3.1 +holidays>=0.20 ipywidgets>=7.5.1 LunarCalendar>=0.0.9 matplotlib>=2.0.0 diff --git a/tests/test_hdays_utils.py b/tests/test_hdays_utils.py new file mode 100644 index 000000000..9e42e1a8a --- /dev/null +++ b/tests/test_hdays_utils.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +import holidays +import pytest + +from neuralprophet import hdays_utils + + +def test_get_country_holidays(): + assert issubclass(hdays_utils.get_country_holidays("TU").__class__, holidays.Turkey) == True + + for country in ("UnitedStates", "US", "USA"): + us_holidays = hdays_utils.get_country_holidays(country, years=2019) + assert issubclass(us_holidays.__class__, holidays.UnitedStates) == True + assert len(us_holidays) == 10 + + with pytest.raises(AttributeError): + hdays_utils.get_country_holidays("NotSupportedCountry") diff --git a/tests/test_utils.py b/tests/test_utils.py index 82f2090ff..4fbd97691 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,7 +8,6 @@ import pytest from neuralprophet import NeuralProphet, load, save -from neuralprophet.hdays_utils import get_country_holidays log = logging.getLogger("NP.test") log.setLevel("DEBUG") @@ -65,9 +64,3 @@ def test_save_load(): # metrics = m.fit(df, freq="D") # metrics2 = m.fit(df, freq="D", continue_training=True) # assert metrics1["Loss"].sum() >= metrics2["Loss"].sum() - - -def test_get_country_holidays(): - holidays = get_country_holidays("US", years=2019) - assert holidays is not None - assert len(holidays) == 10 From 5028d5e03ed04ea9b040f31b3fdf5e355ae81ed9 Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Mon, 27 Feb 2023 16:33:23 -0800 Subject: [PATCH 2/2] Fix pyright file path error. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a76c28376..2f5c64e91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,4 +30,4 @@ profile = "black" line_length = 120 [tool.pyright] -include = ["neuralprophet/forecaster.py", "neuralprophet/configure.py", "neuralprophet/df_utils.py", "neuralprophet/hdays.py"] \ No newline at end of file +include = ["neuralprophet/forecaster.py", "neuralprophet/configure.py", "neuralprophet/df_utils.py", "neuralprophet/hdays_utils.py"] \ No newline at end of file