Update python-holidays integration (#2379)

This commit is contained in:
Arkadii Yakovets 2023-05-28 00:31:59 -07:00 committed by GitHub
parent 49e455a86f
commit 8cf3ba9e46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 51901 additions and 34548 deletions

File diff suppressed because it is too large Load diff

View file

@ -6,13 +6,32 @@
from __future__ import absolute_import, division, print_function
import warnings
import numpy as np
import pandas as pd
import prophet.hdays as hdays_part2
import holidays as hdays_part1
import holidays
def get_country_holidays_class(country):
"""Get class for a supported country.
Parameters
----------
country: country code
Returns
-------
A valid country holidays class
"""
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)
def get_holiday_names(country):
@ -26,18 +45,8 @@ def get_holiday_names(country):
-------
A set of all possible holiday names of given country
"""
years = np.arange(1995, 2045)
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
holiday_names = getattr(hdays_part2, country)(years=years).values()
except AttributeError:
try:
holiday_names = getattr(hdays_part1, country)(years=years).values()
except AttributeError as e:
raise AttributeError(f"Holidays in {country} are not currently supported!") from e
return set(holiday_names)
country_holidays = get_country_holidays_class(country)
return set(country_holidays(language="en_US", years=np.arange(1995, 2045)).values())
def make_holidays_df(year_list, country, province=None, state=None):
@ -53,16 +62,15 @@ def make_holidays_df(year_list, country, province=None, state=None):
Dataframe with 'ds' and 'holiday', which can directly feed
to 'holidays' params in Prophet
"""
try:
holidays = getattr(hdays_part2, country)(years=year_list, expand=False)
except AttributeError:
try:
holidays = getattr(hdays_part1, country)(prov=province, state=state, years=year_list, expand=False)
except AttributeError as e:
raise AttributeError(f"Holidays in {country} are not currently supported!") from e
country_holidays = get_country_holidays_class(country)
holidays = country_holidays(expand=False, language="en_US", subdiv=province, years=year_list)
holidays_df = pd.DataFrame([(date, holidays.get_list(date)) for date in holidays], columns=['ds', 'holiday'])
holidays_df = holidays_df.explode('holiday')
holidays_df = pd.DataFrame(
[(date, holidays.get_list(date)) for date in holidays],
columns=["ds", "holiday"],
)
holidays_df = holidays_df.explode("holiday")
holidays_df.reset_index(inplace=True, drop=True)
holidays_df['ds'] = pd.to_datetime(holidays_df['ds'])
return (holidays_df)
holidays_df["ds"] = pd.to_datetime(holidays_df["ds"])
return holidays_df

View file

@ -19,7 +19,7 @@ dependencies = [
"pandas>=1.0.4",
"LunarCalendar>=0.0.9",
"convertdate>=2.1.2",
"holidays>=0.14.2",
"holidays>=0.25",
"python-dateutil>=2.8.0",
"tqdm>=4.36.1",
"importlib_resources",

View file

@ -8,72 +8,72 @@ from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import inspect
import re
import unicodedata
import warnings
import pandas as pd
import numpy as np
import holidays as hdays_part1
import prophet.hdays as hdays_part2
from holidays import list_supported_countries
from prophet.make_holidays import make_holidays_df
def utf8_to_ascii(text):
"""Holidays often have utf-8 characters. These are not allowed in R
package data (they generate a NOTE).
def utf8_to_ascii(text: str) -> str:
"""Holidays often have utf-8 characters. These are not allowed in R package data (they generate a NOTE).
TODO: revisit whether we want to do this lossy conversion.
"""
ascii_text = (
unicodedata.normalize('NFD', text)
.encode('ascii', 'ignore')
.decode('ascii')
.strip()
)
ascii_text = unicodedata.normalize("NFD", text).encode("ascii", "ignore").decode("ascii")
# Remove trailing empty brackets and spaces.
ascii_text = re.sub(r"\(\)$", "", ascii_text).strip()
# Check if anything converted
if sum(1 for x in ascii_text if x not in [' ', '(', ')', ',']) == 0:
return 'FAILED_TO_PARSE'
if sum(1 for x in ascii_text if x not in [" ", "(", ")", ","]) == 0:
return "FAILED_TO_PARSE"
else:
return ascii_text
def generate_holidays_file():
"""Generate csv file of all possible holiday names, ds,
and countries, year combination
"""
year_list = np.arange(1995, 2045, 1).tolist()
def generate_holidays_df() -> pd.DataFrame:
"""Generate csv file of all possible holiday names, ds, and countries, year combination."""
country_codes = set(list_supported_countries().keys())
# For compatibility with Turkey as 'TU' cases.
country_codes.add("TU")
all_holidays = []
# class names in holiday packages which are not countries
# Also cut out countries with utf-8 holidays that don't parse to ascii
class_to_exclude = {'rd', 'BY', 'BG', 'JP', 'RS', 'UA', 'KR'}
class_list2 = inspect.getmembers(hdays_part2, inspect.isclass)
country_set = {name for name in list(zip(*class_list2))[0] if len(name) == 2}
class_list1 = inspect.getmembers(hdays_part1, inspect.isclass)
country_set1 = {name for name in list(zip(*class_list1))[0] if len(name) == 2}
country_set.update(country_set1)
country_set -= class_to_exclude
for country in country_set:
df = make_holidays_df(year_list=year_list, country=country)
df['country'] = country
for country_code in country_codes:
df = make_holidays_df(
year_list=np.arange(1995, 2045, 1).tolist(),
country=country_code,
)
df["country"] = country_code
all_holidays.append(df)
generated_holidays = pd.concat(all_holidays, axis=0, ignore_index=True)
generated_holidays['year'] = generated_holidays.ds.dt.year
generated_holidays.sort_values(['country', 'ds', 'holiday'], inplace=True)
generated_holidays["year"] = generated_holidays.ds.dt.year
generated_holidays.sort_values(["country", "ds", "holiday"], inplace=True)
# Convert to ASCII, and drop holidays that fail to convert
generated_holidays['holiday'] = generated_holidays['holiday'].apply(utf8_to_ascii)
failed_countries = generated_holidays.loc[generated_holidays['holiday'] == 'FAILED_TO_PARSE', 'country'].unique()
generated_holidays["holiday"] = generated_holidays["holiday"].apply(utf8_to_ascii)
failed_countries = generated_holidays.loc[
generated_holidays["holiday"] == "FAILED_TO_PARSE", "country"
].unique()
if len(failed_countries) > 0:
print("Failed to convert UTF-8 holidays for:")
print('\n'.join(failed_countries))
assert 'FAILED_TO_PARSE' not in generated_holidays['holiday'].unique()
generated_holidays.to_csv("../R/data-raw/generated_holidays.csv", index=False)
print("\n".join(failed_countries))
assert "FAILED_TO_PARSE" not in generated_holidays["holiday"].unique()
return generated_holidays
if __name__ == "__main__":
# execute only if run as a script
generate_holidays_file()
import argparse
import pathlib
if not pathlib.Path.cwd().stem == "python":
raise RuntimeError("Run script from prophet/python directory")
OUT_CSV_PATH = pathlib.Path(".") / ".." / "R/data-raw/generated_holidays.csv"
parser = argparse.ArgumentParser()
parser.add_argument("-o", "--outfile", default=OUT_CSV_PATH)
args = parser.parse_args()
df = generate_holidays_df()
df.to_csv(args.outfile, index=False)

View file

@ -1,6 +0,0 @@
# Copyright (c) Facebook, Inc. and its affiliates.
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
from prophet.hdays import *