From 204efc8bdcac7df240009e0394985789f9e7b788 Mon Sep 17 00:00:00 2001 From: Edrin Basha Date: Fri, 22 May 2020 15:43:13 +0200 Subject: [PATCH] =?UTF-8?q?Add=20a=20new=20CustomDateFeatureSet=20class=20?= =?UTF-8?q?to=20calculate=20holiday=20features=20ba=E2=80=A6=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add a new CustomDateFeatureSet class to calculate holiday features based on an array of pandas timestamps Add a new CustomDateFeatureSet class to calculate holiday features based on an array of pandas timestamps * Added test case for the CustomDateFeatureSet Added test case for the CustomDateFeatureSet * Added new class in the init file Added new class in the init file --- pts/feature/__init__.py | 2 +- pts/feature/holiday.py | 74 ++++++++++++++++++++++++++++++++++++ test/feature/test_holiday.py | 18 +++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) diff --git a/pts/feature/__init__.py b/pts/feature/__init__.py index c85f7f7..7d921fe 100644 --- a/pts/feature/__init__.py +++ b/pts/feature/__init__.py @@ -1,4 +1,4 @@ -from .holiday import SPECIAL_DATE_FEATURES, SpecialDateFeatureSet +from .holiday import SPECIAL_DATE_FEATURES, SpecialDateFeatureSet, CustomDateFeatureSet 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 5d5e69c..e23ba52 100644 --- a/pts/feature/holiday.py +++ b/pts/feature/holiday.py @@ -219,3 +219,77 @@ class SpecialDateFeatureSet: for feat_name in self.feature_names ] ) + +class CustomDateFeatureSet: + """ + Implements calculation of holiday features. The CustomDateFeatureSet 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: + + >>> import pandas as pd + >>> cfs = CustomDateFeatureSet([pd.to_datetime('20191129', format='%Y%m%d'), pd.to_datetime('20200101', format='%Y%m%d')], kernel) + >>> date_indices = pd.date_range( + ... start="2019-11-24", + ... end="2019-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) + >>> cfs = CustomDateFeatureSet([pd.to_datetime('20191129', format='%Y%m%d'), pd.to_datetime('20200101', format='%Y%m%d')], kernel) + >>> cfs(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, + reference_dates: List[pd.Timestamp], + kernel_function: Callable[[int], int] = indicator, + ): + """ + Parameters + ---------- + reference_dates + list of panda timestamps 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.reference_dates = reference_dates + 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((index - ref_date).days) + for index in dates + ] + ) + for ref_date in self.reference_dates + ] + ) diff --git a/test/feature/test_holiday.py b/test/feature/test_holiday.py index b45dd25..64b1ec0 100644 --- a/test/feature/test_holiday.py +++ b/test/feature/test_holiday.py @@ -39,6 +39,7 @@ from pts.feature.holiday import ( CYBER_MONDAY, SpecialDateFeatureSet, squared_exponential_kernel, + CustomDateFeatureSet ) test_dates = { @@ -253,4 +254,21 @@ def test_special_date_feature_set_daily_squared_exponential(): sfs = SpecialDateFeatureSet([CHRISTMAS_EVE, CHRISTMAS_DAY], squared_exp_kernel) computed_features = sfs(date_indices) np.testing.assert_almost_equal(computed_features, reference_features, decimal=6) + +def test_custom_date_feature_set(): + + ref_dates = [pd.to_datetime('20191129', format='%Y%m%d'), pd.to_datetime('20200101', format='%Y%m%d')] + + kernel = exponential_kernel(alpha=1.0) + + cfs = CustomDateFeatureSet(ref_dates, kernel) + sfs = SpecialDateFeatureSet([BLACK_FRIDAY, NEW_YEARS_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" +