diff --git a/pts/feature/__init__.py b/pts/feature/__init__.py index 7d921fe..e658636 100644 --- a/pts/feature/__init__.py +++ b/pts/feature/__init__.py @@ -1,4 +1,4 @@ -from .holiday import SPECIAL_DATE_FEATURES, SpecialDateFeatureSet, CustomDateFeatureSet +from .holiday import SPECIAL_DATE_FEATURES, SpecialDateFeatureSet, CustomDateFeatureSet, CustomHolidayFeatureSet from .lag import get_lags_for_frequency, get_fourier_lags_for_frequency from .time_feature import ( DayOfMonth, diff --git a/pts/feature/holiday.py b/pts/feature/holiday.py index e23ba52..4097ee3 100644 --- a/pts/feature/holiday.py +++ b/pts/feature/holiday.py @@ -293,3 +293,84 @@ class CustomDateFeatureSet: for ref_date in self.reference_dates ] ) + +class CustomHolidayFeatureSet: + """ + Implements calculation of holiday features. The CustomHolidayFeatureSet is + applied on a pandas Series with Datetimeindex and returns a 2D array of + the shape (len(dates), num_features), where num_features are the number + of holidays. + + Note that for lower than daily granularity the distance to the holiday is + still computed on a per-day basis. + + Example use: + + >>> from gluonts.time_feature.holiday import ( + ... squared_exponential_kernel, + ... SpecialDateFeatureSet, + ... CHRISTMAS_DAY, + ... CHRISTMAS_EVE + ... ) + >>> import pandas as pd + >>> from pandas.tseries.holiday import Holiday + >>> cfs = CustomHolidayFeatureSet([Holiday("New Years Day", month=1, day=1), Holiday("Christmas Day", month=12, day=25)]) + >>> date_indices = pd.date_range( + ... start="2016-12-24", + ... end="2016-12-31", + ... freq='D' + ... ) + >>> cfs(date_indices) + array([[1., 0., 0., 0., 0., 0., 0., 0.], + [0., 1., 0., 0., 0., 0., 0., 0.]]) + + Example use for using a squared exponential kernel: + + >>> kernel = squared_exponential_kernel(alpha=1.0) + >>> sfs = SpecialDateFeatureSet([Holiday("New Years Day", month=1, day=1), Holiday("Christmas Day", month=12, day=25)], kernel) + >>> sfs(date_indices) + array([[1.00000000e+00, 3.67879441e-01, 1.83156389e-02, 1.23409804e-04, + 1.12535175e-07, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [3.67879441e-01, 1.00000000e+00, 3.67879441e-01, 1.83156389e-02, + 1.23409804e-04, 1.12535175e-07, 0.00000000e+00, 0.00000000e+00]]) + + """ + + def __init__( + self, + custom_holidays: List[Holiday], + kernel_function: Callable[[int], int] = indicator, + ): + """ + Parameters + ---------- + feature_names + list of strings with holiday names for which features should be created. + kernel_function + kernel function to pass the feature value based + on distance in days. Can be indicator function (default), + exponential_kernel, squared_exponential_kernel or user defined. + """ + self.custom_holidays = custom_holidays + self.kernel_function = kernel_function + + def __call__(self, dates): + """ + Transform a pandas series with timestamps to holiday features. + + Parameters + ---------- + dates + Pandas series with Datetimeindex timestamps. + """ + return np.vstack( + [ + np.hstack( + [ + self.kernel_function(distance_to_holiday(custom_holiday)(index)) + for index in dates + ] + ) + for custom_holiday in self.custom_holidays + ] + ) diff --git a/test/feature/test_holiday.py b/test/feature/test_holiday.py index 64b1ec0..582901f 100644 --- a/test/feature/test_holiday.py +++ b/test/feature/test_holiday.py @@ -15,6 +15,8 @@ import numpy as np import pandas as pd import pytest +from pandas.tseries.holiday import Holiday + # First-party imports from pts.feature.holiday import ( @@ -272,3 +274,18 @@ def test_custom_date_feature_set(): assert(np.sum(cfs(date_indices) - sfs(date_indices)) == 0), "Features don't match" +def test_custom_holiday_feature_set(): + + custom_holidays = [Holiday("New Years Day", month=1, day=1), Holiday("Christmas Day", month=12, day=25)] + + kernel = exponential_kernel(alpha=1.0) + + cfs = CustomHolidayFeatureSet(custom_holidays, kernel) + sfs = SpecialDateFeatureSet([NEW_YEARS_DAY, CHRISTMAS_DAY], kernel) + + date_indices = pd.date_range( + start=pd.to_datetime('20191101', format='%Y%m%d'), + end=pd.to_datetime('20200131', format='%Y%m%d'), + freq='D') + + assert(np.sum(cfs(date_indices) - sfs(date_indices)) == 0), "Features don't match"