# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License").
# You may not use this file except in compliance with the License.
# A copy of the License is located at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# or in the "license" file accompanying this file. This file is distributed
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
# express or implied. See the License for the specific language governing
# permissions and limitations under the License.
from typing import List
import numpy as np
import pandas as pd
from pandas.tseries import offsets
from pandas.tseries.frequencies import to_offset
from gluonts.core.component import validated
[docs]class TimeFeature:
"""
Base class for features that only depend on time.
"""
@validated()
def __init__(self):
pass
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
pass
def __repr__(self):
return self.__class__.__name__ + "()"
[docs]class SecondOfMinute(TimeFeature):
"""
Second of minute encoded as value between [-0.5, 0.5]
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return index.second.values / 59.0 - 0.5
[docs]class SecondOfMinuteIndex(TimeFeature):
"""
Second of minute encoded as zero-based index, between 0 and 59.
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return index.second.astype(float).values
[docs]class MinuteOfHour(TimeFeature):
"""
Minute of hour encoded as value between [-0.5, 0.5]
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return index.minute.values / 59.0 - 0.5
[docs]class MinuteOfHourIndex(TimeFeature):
"""
Minute of hour encoded as zero-based index, between 0 and 59.
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return index.minute.astype(float).values
[docs]class HourOfDay(TimeFeature):
"""
Hour of day encoded as value between [-0.5, 0.5]
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return index.hour.values / 23.0 - 0.5
[docs]class HourOfDayIndex(TimeFeature):
"""
Hour of day encoded as zero-based index, between 0 and 23.
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return index.hour.astype(float).values
[docs]class DayOfWeek(TimeFeature):
"""
Hour of day encoded as value between [-0.5, 0.5]
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return index.dayofweek.values / 6.0 - 0.5
[docs]class DayOfWeekIndex(TimeFeature):
"""
Hour of day encoded as zero-based index, between 0 and 6.
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return index.dayofweek.astype(float).values
[docs]class DayOfMonth(TimeFeature):
"""
Day of month encoded as value between [-0.5, 0.5]
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return (index.day.values - 1) / 30.0 - 0.5
[docs]class DayOfMonthIndex(TimeFeature):
"""
Day of month encoded as zero-based index, between 0 and 11.
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return index.day.astype(float).values - 1
[docs]class DayOfYear(TimeFeature):
"""
Day of year encoded as value between [-0.5, 0.5]
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return (index.dayofyear.values - 1) / 365.0 - 0.5
[docs]class DayOfYearIndex(TimeFeature):
"""
Day of year encoded as zero-based index, between 0 and 365.
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return index.dayofyear.astype(float).values - 1
[docs]class MonthOfYear(TimeFeature):
"""
Month of year encoded as value between [-0.5, 0.5]
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return (index.month.values - 1) / 11.0 - 0.5
[docs]class MonthOfYearIndex(TimeFeature):
"""
Month of year encoded as zero-based index, between 0 and 11.
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return index.month.astype(float).values - 1
[docs]class WeekOfYear(TimeFeature):
"""
Week of year encoded as value between [-0.5, 0.5]
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
# TODO:
# * pandas >= 1.1 does not support `.week`
# * pandas == 1.0 does not support `.isocalendar()`
# as soon as we drop support for `pandas == 1.0`, we should remove this
try:
week = index.isocalendar().week
except AttributeError:
week = index.week
return (week.astype(float).values - 1) / 52.0 - 0.5
[docs]class WeekOfYearIndex(TimeFeature):
"""
Week of year encoded as zero-based index, between 0 and 52.
"""
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
# TODO:
# * pandas >= 1.1 does not support `.week`
# * pandas == 1.0 does not support `.isocalendar()`
# as soon as we drop support for `pandas == 1.0`, we should remove this
try:
week = index.isocalendar().week
except AttributeError:
week = index.week
return week.astype(float).values - 1
[docs]class Constant(TimeFeature):
"""
Constant time feature using a predefined value.
"""
@validated()
def __init__(self, value: float = 0.0):
super().__init__()
self.value = value
def __call__(self, index: pd.PeriodIndex) -> np.ndarray:
return np.full(index.shape, self.value)
[docs]def norm_freq_str(freq_str: str) -> str:
return freq_str.split("-")[0]
[docs]def time_features_from_frequency_str(freq_str: str) -> List[TimeFeature]:
"""
Returns a list of time features that will be appropriate for the given
frequency string.
Parameters
----------
freq_str
Frequency string of the form [multiple][granularity] such as "12H",
"5min", "1D" etc.
"""
features_by_offsets = {
offsets.YearBegin: [],
offsets.YearEnd: [],
offsets.QuarterBegin: [MonthOfYear],
offsets.QuarterEnd: [MonthOfYear],
offsets.MonthBegin: [MonthOfYear],
offsets.MonthEnd: [MonthOfYear],
offsets.Week: [DayOfMonth, WeekOfYear],
offsets.Day: [DayOfWeek, DayOfMonth, DayOfYear],
offsets.BusinessDay: [DayOfWeek, DayOfMonth, DayOfYear],
offsets.Hour: [HourOfDay, DayOfWeek, DayOfMonth, DayOfYear],
offsets.Minute: [
MinuteOfHour,
HourOfDay,
DayOfWeek,
DayOfMonth,
DayOfYear,
],
offsets.Second: [
SecondOfMinute,
MinuteOfHour,
HourOfDay,
DayOfWeek,
DayOfMonth,
DayOfYear,
],
}
offset = to_offset(freq_str)
for offset_type, feature_classes in features_by_offsets.items():
if isinstance(offset, offset_type):
return [cls() for cls in feature_classes]
supported_freq_msg = f"""
Unsupported frequency {freq_str}
The following frequencies are supported:
Y - yearly
alias: A
Q - quarterly
M - monthly
W - weekly
D - daily
B - business days
H - hourly
T - minutely
alias: min
S - secondly
"""
raise RuntimeError(supported_freq_msg)