2022-03-29 05:05:20 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-02-19 17:33:00 +00:00
|
|
|
import datetime
|
2022-02-27 13:56:03 +00:00
|
|
|
import inspect
|
2022-04-10 18:22:53 +00:00
|
|
|
import warnings
|
2022-04-10 08:39:24 +00:00
|
|
|
from collections import UserList
|
2022-02-19 17:33:00 +00:00
|
|
|
from dataclasses import dataclass
|
2022-02-20 17:19:45 +00:00
|
|
|
from numbers import Number
|
2022-04-05 05:13:53 +00:00
|
|
|
from typing import Callable, Iterable, List, Literal, Mapping, Sequence, Type
|
2022-02-19 17:33:00 +00:00
|
|
|
|
2022-03-22 15:59:58 +00:00
|
|
|
from .utils import FincalOptions, _parse_date, _preprocess_timeseries
|
2022-02-19 17:33:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
class Frequency:
|
|
|
|
name: str
|
|
|
|
freq_type: str
|
|
|
|
value: int
|
|
|
|
days: int
|
2022-02-20 10:36:34 +00:00
|
|
|
symbol: str
|
2022-02-19 17:33:00 +00:00
|
|
|
|
|
|
|
|
2022-03-01 10:04:16 +00:00
|
|
|
def date_parser(*pos):
|
2022-03-05 17:53:31 +00:00
|
|
|
"""Decorator to parse dates in any function
|
|
|
|
|
|
|
|
Accepts the 0-indexed position of the parameter for which date parsing needs to be done.
|
|
|
|
Works even if function is used with keyword arguments while not maintaining parameter order.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
--------
|
|
|
|
>>> @date_parser(2, 3)
|
|
|
|
>>> def calculate_difference(diff_units='days', return_type='int', date1, date2):
|
|
|
|
... diff = date2 - date1
|
|
|
|
... if return_type == 'int':
|
|
|
|
... return diff.days
|
|
|
|
... return diff
|
|
|
|
...
|
2022-03-12 04:54:40 +00:00
|
|
|
>>> calculate_difference(date1='2019-01-01', date2='2020-01-01')
|
2022-03-05 17:53:31 +00:00
|
|
|
datetime.timedelta(365)
|
|
|
|
|
|
|
|
Each of the dates is automatically parsed into a datetime.datetime object from string.
|
|
|
|
"""
|
2022-03-11 04:12:22 +00:00
|
|
|
|
2022-02-27 10:59:18 +00:00
|
|
|
def parse_dates(func):
|
|
|
|
def wrapper_func(*args, **kwargs):
|
2022-04-05 05:13:53 +00:00
|
|
|
date_format: str = kwargs.get("date_format", None)
|
|
|
|
args: list = list(args)
|
|
|
|
sig: inspect.Signature = inspect.signature(func)
|
|
|
|
params: list = [i[0] for i in sig.parameters.items()]
|
2022-03-01 10:04:16 +00:00
|
|
|
|
2022-02-27 13:56:03 +00:00
|
|
|
for j in pos:
|
2022-04-05 05:13:53 +00:00
|
|
|
kwarg: str = params[j]
|
2022-02-27 13:56:03 +00:00
|
|
|
date = kwargs.get(kwarg, None)
|
2022-04-05 05:13:53 +00:00
|
|
|
in_args: bool = False
|
2022-02-27 10:59:18 +00:00
|
|
|
if date is None:
|
2022-03-11 04:12:22 +00:00
|
|
|
try:
|
|
|
|
date = args[j]
|
|
|
|
except IndexError:
|
|
|
|
pass
|
2022-02-27 10:59:18 +00:00
|
|
|
in_args = True
|
|
|
|
|
2022-03-11 04:12:22 +00:00
|
|
|
if date is None:
|
|
|
|
continue
|
|
|
|
|
2022-04-05 05:13:53 +00:00
|
|
|
parsed_date: datetime.datetime = _parse_date(date, date_format)
|
2022-02-27 10:59:18 +00:00
|
|
|
if not in_args:
|
2022-02-27 13:56:03 +00:00
|
|
|
kwargs[kwarg] = parsed_date
|
2022-02-27 10:59:18 +00:00
|
|
|
else:
|
2022-02-27 13:56:03 +00:00
|
|
|
args[j] = parsed_date
|
2022-02-27 10:59:18 +00:00
|
|
|
return func(*args, **kwargs)
|
|
|
|
|
|
|
|
return wrapper_func
|
|
|
|
|
|
|
|
return parse_dates
|
|
|
|
|
|
|
|
|
2022-02-19 17:33:00 +00:00
|
|
|
class AllFrequencies:
|
2022-02-21 07:39:58 +00:00
|
|
|
D = Frequency("daily", "days", 1, 1, "D")
|
|
|
|
W = Frequency("weekly", "days", 7, 7, "W")
|
|
|
|
M = Frequency("monthly", "months", 1, 30, "M")
|
|
|
|
Q = Frequency("quarterly", "months", 3, 91, "Q")
|
|
|
|
H = Frequency("half-yearly", "months", 6, 182, "H")
|
|
|
|
Y = Frequency("annual", "years", 1, 365, "Y")
|
2022-02-19 17:33:00 +00:00
|
|
|
|
|
|
|
|
2022-02-21 02:56:29 +00:00
|
|
|
class _IndexSlicer:
|
2022-02-21 17:37:43 +00:00
|
|
|
"""Class to create a slice using iloc in TimeSeriesCore"""
|
|
|
|
|
2022-03-12 04:54:40 +00:00
|
|
|
def __init__(self, parent_obj: object):
|
2022-02-20 13:30:39 +00:00
|
|
|
self.parent = parent_obj
|
|
|
|
|
|
|
|
def __getitem__(self, n):
|
|
|
|
if isinstance(n, int):
|
2022-04-05 05:13:53 +00:00
|
|
|
keys: list = [self.parent.dates[n]]
|
2022-02-20 13:30:39 +00:00
|
|
|
else:
|
2022-04-05 05:13:53 +00:00
|
|
|
keys: list = self.parent.dates[n]
|
2022-02-21 17:37:43 +00:00
|
|
|
item = [(key, self.parent.data[key]) for key in keys]
|
2022-02-20 13:30:39 +00:00
|
|
|
if len(item) == 1:
|
|
|
|
return item[0]
|
|
|
|
|
2022-03-12 04:54:40 +00:00
|
|
|
return self.parent.__class__(item, self.parent.frequency.symbol)
|
2022-02-20 13:30:39 +00:00
|
|
|
|
2022-04-10 18:22:53 +00:00
|
|
|
def __setitem__(self, key, value):
|
|
|
|
raise NotImplementedError(
|
|
|
|
"iloc cannot be used for setting a value as value will always be inserted in order of date"
|
|
|
|
)
|
|
|
|
|
2022-02-20 13:30:39 +00:00
|
|
|
|
2022-02-21 07:39:58 +00:00
|
|
|
class Series(UserList):
|
2022-02-21 16:57:26 +00:00
|
|
|
"""Container for a series of objects, all objects must be of the same type"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
2022-04-05 05:13:53 +00:00
|
|
|
data: Sequence,
|
2022-02-23 18:45:59 +00:00
|
|
|
data_type: Literal["date", "number", "bool"],
|
2022-02-21 16:57:26 +00:00
|
|
|
date_format: str = None,
|
|
|
|
):
|
2022-04-05 05:13:53 +00:00
|
|
|
types_dict: dict = {
|
2022-02-23 18:45:59 +00:00
|
|
|
"date": datetime.datetime,
|
|
|
|
"datetime": datetime.datetime,
|
|
|
|
"datetime.datetime": datetime.datetime,
|
|
|
|
"float": float,
|
|
|
|
"int": float,
|
|
|
|
"number": float,
|
|
|
|
"bool": bool,
|
2022-02-22 05:58:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if data_type not in types_dict.keys():
|
|
|
|
raise ValueError("Unsupported value for data type")
|
|
|
|
|
2022-02-20 17:19:45 +00:00
|
|
|
if not isinstance(data, Sequence):
|
2022-02-21 16:57:26 +00:00
|
|
|
raise TypeError("Series object can only be created using Sequence types")
|
|
|
|
|
2022-02-23 18:45:59 +00:00
|
|
|
if data_type in ["date", "datetime", "datetime.datetime"]:
|
2022-02-21 16:57:26 +00:00
|
|
|
data = [_parse_date(i, date_format) for i in data]
|
2022-02-22 05:58:26 +00:00
|
|
|
else:
|
2022-04-05 05:13:53 +00:00
|
|
|
func: Callable = types_dict[data_type]
|
|
|
|
data: list = [func(i) for i in data]
|
2022-02-22 05:58:26 +00:00
|
|
|
|
2022-04-05 05:13:53 +00:00
|
|
|
self.dtype: Type = types_dict[data_type]
|
|
|
|
self.data: Sequence = data
|
2022-02-20 17:19:45 +00:00
|
|
|
|
|
|
|
def __repr__(self):
|
2022-02-23 18:30:52 +00:00
|
|
|
return f"{self.__class__.__name__}({self.data}, data_type='{self.dtype.__name__}')"
|
2022-02-20 17:19:45 +00:00
|
|
|
|
2022-02-21 17:18:00 +00:00
|
|
|
def __getitem__(self, i):
|
|
|
|
if isinstance(i, slice):
|
2022-02-22 05:58:26 +00:00
|
|
|
return self.__class__(self.data[i], str(self.dtype.__name__))
|
2022-02-21 17:18:00 +00:00
|
|
|
else:
|
|
|
|
return self.data[i]
|
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
def _comparison_validator(self, other):
|
|
|
|
"""Validates other before making comparison"""
|
2022-02-20 17:19:45 +00:00
|
|
|
|
2022-02-21 07:39:58 +00:00
|
|
|
if isinstance(other, (str, datetime.datetime, datetime.date)):
|
|
|
|
other = _parse_date(other)
|
2022-04-11 05:17:12 +00:00
|
|
|
return other
|
2022-02-21 07:39:58 +00:00
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
if self.dtype == bool:
|
|
|
|
raise TypeError("Comparison operation not supported for boolean series")
|
|
|
|
|
|
|
|
elif isinstance(other, Series):
|
2022-04-10 18:22:53 +00:00
|
|
|
if len(self) != len(other):
|
|
|
|
raise ValueError("Length of Series must be same for comparison")
|
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
elif (self.dtype != float and isinstance(other, Number)) or not isinstance(other, self.dtype):
|
2022-02-20 17:19:45 +00:00
|
|
|
raise Exception(f"Cannot compare type {self.dtype.__name__} to {type(other).__name__}")
|
|
|
|
|
2022-04-11 16:49:29 +00:00
|
|
|
return other
|
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
def __gt__(self, other):
|
|
|
|
other = self._comparison_validator(other)
|
2022-02-22 05:58:26 +00:00
|
|
|
|
2022-04-10 18:22:53 +00:00
|
|
|
if isinstance(other, Series):
|
2022-04-11 05:17:12 +00:00
|
|
|
return Series([j > other[i] for i, j in enumerate(self)], "bool")
|
2022-04-10 18:22:53 +00:00
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
return Series([i > other for i in self.data], "bool")
|
2022-04-10 18:22:53 +00:00
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
def __ge__(self, other):
|
|
|
|
other = self._comparison_validator(other)
|
2022-02-22 05:58:26 +00:00
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
if isinstance(other, Series):
|
|
|
|
return Series([j >= other[i] for i, j in enumerate(self)], "bool")
|
2022-02-22 05:58:26 +00:00
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
return Series([i >= other for i in self.data], "bool")
|
2022-02-20 17:19:45 +00:00
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
def __lt__(self, other):
|
|
|
|
other = self._comparison_validator(other)
|
2022-02-22 05:58:26 +00:00
|
|
|
|
2022-04-10 18:22:53 +00:00
|
|
|
if isinstance(other, Series):
|
2022-04-11 05:17:12 +00:00
|
|
|
return Series([j < other[i] for i, j in enumerate(self)], "bool")
|
2022-04-10 18:22:53 +00:00
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
return Series([i < other for i in self.data], "bool")
|
2022-02-20 17:19:45 +00:00
|
|
|
|
2022-02-22 05:58:26 +00:00
|
|
|
def __le__(self, other):
|
2022-04-11 05:17:12 +00:00
|
|
|
other = self._comparison_validator(other)
|
2022-02-22 05:58:26 +00:00
|
|
|
|
2022-04-10 18:22:53 +00:00
|
|
|
if isinstance(other, Series):
|
2022-04-11 05:17:12 +00:00
|
|
|
return Series([j <= other[i] for i, j in enumerate(self)], "bool")
|
2022-04-10 18:22:53 +00:00
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
return Series([i <= other for i in self.data], "bool")
|
2022-02-22 05:58:26 +00:00
|
|
|
|
2022-02-20 17:19:45 +00:00
|
|
|
def __eq__(self, other):
|
2022-04-11 05:17:12 +00:00
|
|
|
other = self._comparison_validator(other)
|
2022-02-22 05:58:26 +00:00
|
|
|
|
2022-04-10 18:22:53 +00:00
|
|
|
if isinstance(other, Series):
|
2022-04-11 05:17:12 +00:00
|
|
|
return Series([j == other[i] for i, j in enumerate(self)], "bool")
|
2022-04-10 18:22:53 +00:00
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
return Series([i == other for i in self.data], "bool")
|
2022-02-20 17:19:45 +00:00
|
|
|
|
2022-04-11 16:49:29 +00:00
|
|
|
def __ne__(self, other):
|
|
|
|
other = self._comparison_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
return Series([j != other[i] for i, j in enumerate(self)], "bool")
|
|
|
|
|
|
|
|
return Series([i == other for i in self.data], "bool")
|
|
|
|
|
2022-02-20 17:19:45 +00:00
|
|
|
|
2022-04-10 18:22:53 +00:00
|
|
|
@Mapping.register
|
2022-04-10 08:39:24 +00:00
|
|
|
class TimeSeriesCore:
|
2022-02-19 17:33:00 +00:00
|
|
|
"""Defines the core building blocks of a TimeSeries object"""
|
|
|
|
|
|
|
|
def __init__(
|
2022-04-05 05:13:53 +00:00
|
|
|
self,
|
2022-04-07 18:14:18 +00:00
|
|
|
ts_data: List[Iterable] | Mapping,
|
2022-04-05 05:13:53 +00:00
|
|
|
frequency: Literal["D", "W", "M", "Q", "H", "Y"],
|
|
|
|
date_format: str = "%Y-%m-%d",
|
2022-02-19 17:33:00 +00:00
|
|
|
):
|
2022-02-27 09:19:50 +00:00
|
|
|
"""Instantiate a TimeSeriesCore object
|
2022-02-19 17:33:00 +00:00
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
2022-04-07 18:14:18 +00:00
|
|
|
ts_data : List[Iterable] | Mapping
|
2022-04-05 05:13:53 +00:00
|
|
|
Time Series data in the form of list of tuples or dictionary.
|
2022-02-19 17:33:00 +00:00
|
|
|
The first element of each tuple should be a date and second element should be a value.
|
2022-04-05 05:13:53 +00:00
|
|
|
In case of dictionary, the key should be the date.
|
|
|
|
|
|
|
|
frequency : str
|
|
|
|
The frequency of the time series.
|
|
|
|
Valid values are {D, W, M, Q, H, Y}
|
2022-02-19 17:33:00 +00:00
|
|
|
|
|
|
|
date_format : str, optional, default "%Y-%m-%d"
|
|
|
|
Specify the format of the date
|
|
|
|
Required only if the first argument of tuples is a string. Otherwise ignored.
|
|
|
|
"""
|
|
|
|
|
2022-04-07 18:14:18 +00:00
|
|
|
ts_data = _preprocess_timeseries(ts_data, date_format=date_format)
|
2022-02-19 17:33:00 +00:00
|
|
|
|
2022-04-10 08:39:24 +00:00
|
|
|
self.data = dict(ts_data)
|
2022-04-07 18:14:18 +00:00
|
|
|
if len(self.data) != len(ts_data):
|
2022-04-10 18:22:53 +00:00
|
|
|
warnings.warn("The input data contains duplicate dates which have been ignored.")
|
2022-03-21 15:17:22 +00:00
|
|
|
self.frequency: Frequency = getattr(AllFrequencies, frequency)
|
|
|
|
self.iter_num: int = -1
|
|
|
|
self._dates: list = None
|
|
|
|
self._values: list = None
|
|
|
|
self._start_date: datetime.datetime = None
|
|
|
|
self._end_date: datetime.datetime = None
|
2022-02-20 16:22:33 +00:00
|
|
|
|
|
|
|
@property
|
2022-02-26 18:52:08 +00:00
|
|
|
def dates(self) -> Series:
|
|
|
|
"""Get a list of all the dates in the TimeSeries object"""
|
|
|
|
|
2022-02-21 09:53:20 +00:00
|
|
|
if self._dates is None or len(self._dates) != len(self.data):
|
|
|
|
self._dates = list(self.data.keys())
|
2022-02-20 16:22:33 +00:00
|
|
|
|
2022-02-23 18:45:59 +00:00
|
|
|
return Series(self._dates, "date")
|
2022-02-20 16:22:33 +00:00
|
|
|
|
|
|
|
@property
|
2022-02-26 18:52:08 +00:00
|
|
|
def values(self) -> Series:
|
|
|
|
"""Get a list of all the Values in the TimeSeries object"""
|
|
|
|
|
2022-02-21 09:53:20 +00:00
|
|
|
if self._values is None or len(self._values) != len(self.data):
|
|
|
|
self._values = list(self.data.values())
|
2022-02-20 16:22:33 +00:00
|
|
|
|
2022-02-23 18:45:59 +00:00
|
|
|
return Series(self._values, "number")
|
2022-02-20 16:22:33 +00:00
|
|
|
|
|
|
|
@property
|
2022-02-26 18:52:08 +00:00
|
|
|
def start_date(self) -> datetime.datetime:
|
|
|
|
"""The first date in the TimeSeries object"""
|
|
|
|
|
2022-02-20 16:22:33 +00:00
|
|
|
return self.dates[0]
|
|
|
|
|
|
|
|
@property
|
2022-02-26 18:52:08 +00:00
|
|
|
def end_date(self) -> datetime.datetime:
|
|
|
|
"""The last date in the TimeSeries object"""
|
|
|
|
|
2022-02-20 16:22:33 +00:00
|
|
|
return self.dates[-1]
|
2022-02-19 17:33:00 +00:00
|
|
|
|
2022-02-20 13:30:39 +00:00
|
|
|
def _get_printable_slice(self, n: int):
|
2022-02-26 18:52:08 +00:00
|
|
|
"""Helper function for __repr__ and __str__
|
|
|
|
|
2022-02-27 10:59:18 +00:00
|
|
|
Returns a slice of the dataframe from beginning and end.
|
2022-02-26 18:52:08 +00:00
|
|
|
"""
|
2022-02-20 10:36:34 +00:00
|
|
|
|
|
|
|
printable = {}
|
2022-02-21 09:53:20 +00:00
|
|
|
iter_f = iter(self.data)
|
2022-02-21 07:39:58 +00:00
|
|
|
first_n = [next(iter_f) for i in range(n // 2)]
|
2022-02-20 10:36:34 +00:00
|
|
|
|
2022-02-21 09:53:20 +00:00
|
|
|
iter_b = reversed(self.data)
|
2022-02-21 07:39:58 +00:00
|
|
|
last_n = [next(iter_b) for i in range(n // 2)]
|
2022-02-20 10:36:34 +00:00
|
|
|
last_n.sort()
|
|
|
|
|
2022-02-21 09:53:20 +00:00
|
|
|
printable["start"] = [str((i, self.data[i])) for i in first_n]
|
|
|
|
printable["end"] = [str((i, self.data[i])) for i in last_n]
|
2022-02-20 10:36:34 +00:00
|
|
|
return printable
|
|
|
|
|
2022-02-19 17:33:00 +00:00
|
|
|
def __repr__(self):
|
2022-02-21 09:53:20 +00:00
|
|
|
if len(self.data) > 6:
|
2022-02-20 13:30:39 +00:00
|
|
|
printable = self._get_printable_slice(6)
|
2022-02-20 10:36:34 +00:00
|
|
|
printable_str = "{}([{}\n\t ...\n\t {}], frequency={})".format(
|
2022-02-21 07:39:58 +00:00
|
|
|
self.__class__.__name__,
|
|
|
|
",\n\t ".join(printable["start"]),
|
|
|
|
",\n\t ".join(printable["end"]),
|
|
|
|
repr(self.frequency.symbol),
|
|
|
|
)
|
2022-02-19 17:33:00 +00:00
|
|
|
else:
|
2022-02-20 10:36:34 +00:00
|
|
|
printable_str = "{}([{}], frequency={})".format(
|
2022-02-21 07:39:58 +00:00
|
|
|
self.__class__.__name__,
|
2022-02-21 09:53:20 +00:00
|
|
|
",\n\t".join([str(i) for i in self.data.items()]),
|
2022-02-21 07:39:58 +00:00
|
|
|
repr(self.frequency.symbol),
|
|
|
|
)
|
2022-02-19 17:33:00 +00:00
|
|
|
return printable_str
|
|
|
|
|
|
|
|
def __str__(self):
|
2022-02-21 09:53:20 +00:00
|
|
|
if len(self.data) > 6:
|
2022-02-20 13:30:39 +00:00
|
|
|
printable = self._get_printable_slice(6)
|
2022-02-19 17:33:00 +00:00
|
|
|
printable_str = "[{}\n ...\n {}]".format(
|
2022-02-21 07:39:58 +00:00
|
|
|
",\n ".join(printable["start"]),
|
|
|
|
",\n ".join(printable["end"]),
|
|
|
|
)
|
2022-02-19 17:33:00 +00:00
|
|
|
else:
|
2022-02-21 09:53:20 +00:00
|
|
|
printable_str = "[{}]".format(",\n ".join([str(i) for i in self.data.items()]))
|
2022-02-19 17:33:00 +00:00
|
|
|
return printable_str
|
|
|
|
|
2022-03-12 04:54:40 +00:00
|
|
|
@date_parser(1)
|
2022-04-05 05:13:53 +00:00
|
|
|
def _get_item_from_date(self, date: str | datetime.datetime):
|
2022-04-11 16:49:29 +00:00
|
|
|
"""Helper function to retrieve item using a date"""
|
|
|
|
|
2022-03-12 04:54:40 +00:00
|
|
|
return date, self.data[date]
|
2022-02-20 17:19:45 +00:00
|
|
|
|
2022-04-05 05:13:53 +00:00
|
|
|
def _get_item_from_key(self, key: str | datetime.datetime):
|
2022-04-11 16:49:29 +00:00
|
|
|
"""Helper function to implement special keys"""
|
|
|
|
|
2022-02-20 13:30:39 +00:00
|
|
|
if isinstance(key, int):
|
2022-03-12 04:54:40 +00:00
|
|
|
raise KeyError(f"{key}. \nHint: use .iloc[{key}] for index based slicing.")
|
|
|
|
|
|
|
|
if key in ["dates", "values"]:
|
|
|
|
return getattr(self, key)
|
|
|
|
|
|
|
|
return self._get_item_from_date(key)
|
|
|
|
|
2022-04-05 05:13:53 +00:00
|
|
|
def _get_item_from_list(self, date_list: Sequence[str | datetime.datetime]):
|
2022-04-11 16:49:29 +00:00
|
|
|
"""Helper function to retrieve items using a list"""
|
|
|
|
|
2022-03-12 04:54:40 +00:00
|
|
|
data_to_return = [self._get_item_from_key(key) for key in date_list]
|
|
|
|
return self.__class__(data_to_return, frequency=self.frequency.symbol)
|
|
|
|
|
|
|
|
def _get_item_from_series(self, series: Series):
|
2022-04-11 16:49:29 +00:00
|
|
|
"""Helper function to retrieve item using a Series object
|
|
|
|
|
|
|
|
A Series of type bool of equal length to the time series can be used.
|
|
|
|
A Series of dates can be used to filter out a set of dates.
|
|
|
|
"""
|
2022-03-12 04:54:40 +00:00
|
|
|
if series.dtype == bool:
|
|
|
|
if len(series) != len(self.dates):
|
|
|
|
raise ValueError(f"Length of Series: {len(series)} did not match length of object: {len(self.dates)}")
|
|
|
|
dates_to_return = [self.dates[i] for i, j in enumerate(series) if j]
|
|
|
|
elif series.dtype == datetime.datetime:
|
|
|
|
dates_to_return = list(series)
|
2022-02-19 17:33:00 +00:00
|
|
|
else:
|
2022-03-12 04:54:40 +00:00
|
|
|
raise TypeError(f"Cannot slice {self.__class__.__name__} using a Series of {series.dtype.__name__}")
|
|
|
|
|
|
|
|
return self._get_item_from_list(dates_to_return)
|
|
|
|
|
|
|
|
def __getitem__(self, key):
|
|
|
|
if isinstance(key, (int, str, datetime.datetime, datetime.date)):
|
|
|
|
return self._get_item_from_key(key)
|
|
|
|
|
|
|
|
if isinstance(key, Series):
|
|
|
|
return self._get_item_from_series(key)
|
|
|
|
|
|
|
|
if isinstance(key, Sequence):
|
|
|
|
return self._get_item_from_list(key)
|
|
|
|
|
|
|
|
raise TypeError(f"Invalid type {repr(type(key).__name__)} for slicing.")
|
2022-02-19 17:33:00 +00:00
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
@date_parser(1)
|
|
|
|
def __setitem__(self, key: str | datetime.datetime, value: Number) -> None:
|
|
|
|
if not isinstance(value, Number):
|
|
|
|
raise TypeError("Only numerical values can be stored in TimeSeries")
|
|
|
|
|
|
|
|
if key in self.data:
|
|
|
|
self.data[key] = value
|
|
|
|
else:
|
|
|
|
self.data.update({key: value})
|
|
|
|
self.data = dict(sorted(self.data.items()))
|
|
|
|
|
|
|
|
@date_parser(1)
|
|
|
|
def __delitem__(self, key):
|
|
|
|
del self.data[key]
|
|
|
|
|
|
|
|
def _comparison_validator(self, other):
|
|
|
|
"""Validates the data before comparison is performed"""
|
|
|
|
|
|
|
|
if not isinstance(other, (Number, Series, TimeSeriesCore)):
|
|
|
|
raise TypeError(
|
|
|
|
f"Comparison cannot be performed between '{self.__class__.__name__}' and '{other.__class__.__name__}'"
|
|
|
|
)
|
2022-04-10 18:22:53 +00:00
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
2022-04-11 16:49:29 +00:00
|
|
|
if any(self.dates != other.dates):
|
2022-04-10 18:22:53 +00:00
|
|
|
raise ValueError(
|
|
|
|
"Only objects with same set of dates can be compared.\n"
|
|
|
|
"Hint: use TimeSeries.sync() method to sync dates of two TimeSeries objects."
|
|
|
|
)
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
2022-04-11 05:17:12 +00:00
|
|
|
if other.dtype != float:
|
2022-04-10 18:22:53 +00:00
|
|
|
raise TypeError("Only Series of type float can be used for comparison")
|
|
|
|
|
|
|
|
if len(self) != len(other):
|
|
|
|
raise ValueError("Length of series does not match length of object")
|
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
def __gt__(self, other):
|
|
|
|
self._comparison_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, Number):
|
|
|
|
data = {k: v > other for k, v in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
data = {dt: val > other[dt][1] for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
2022-04-10 18:22:53 +00:00
|
|
|
data = {dt: val > other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
|
|
|
|
return self.__class__(data, frequency=self.frequency.symbol)
|
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
def __ge__(self, other):
|
|
|
|
self._comparison_validator(other)
|
2022-04-10 18:22:53 +00:00
|
|
|
|
2022-04-11 05:17:12 +00:00
|
|
|
if isinstance(other, Number):
|
|
|
|
data = {k: v >= other for k, v in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
data = {dt: val >= other[dt][1] for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val >= other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
|
|
|
|
return self.__class__(data, frequency=self.frequency.symbol)
|
|
|
|
|
|
|
|
def __lt__(self, other):
|
|
|
|
self._comparison_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, Number):
|
|
|
|
data = {k: v < other for k, v in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
data = {dt: val < other[dt][1] for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val < other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
|
|
|
|
return self.__class__(data, frequency=self.frequency.symbol)
|
|
|
|
|
|
|
|
def __le__(self, other):
|
|
|
|
self._comparison_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, Number):
|
|
|
|
data = {k: v <= other for k, v in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
data = {dt: val <= other[dt][1] for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val <= other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
|
|
|
|
return self.__class__(data, frequency=self.frequency.symbol)
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
self._comparison_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, Number):
|
|
|
|
data = {k: v == other for k, v in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
data = {dt: val == other[dt][1] for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val == other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
|
|
|
|
return self.__class__(data, frequency=self.frequency.symbol)
|
2022-04-10 07:57:25 +00:00
|
|
|
|
2022-04-11 16:49:29 +00:00
|
|
|
def __ne__(self, other):
|
|
|
|
self._comparison_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, Number):
|
|
|
|
data = {k: v != other for k, v in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
data = {dt: val != other[dt][1] for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val != other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
|
|
|
|
return self.__class__(data, frequency=self.frequency.symbol)
|
|
|
|
|
2022-02-20 16:06:44 +00:00
|
|
|
def __iter__(self):
|
|
|
|
self.n = 0
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __next__(self):
|
|
|
|
if self.n >= len(self.dates):
|
|
|
|
raise StopIteration
|
|
|
|
else:
|
|
|
|
key = self.dates[self.n]
|
|
|
|
self.n += 1
|
2022-02-21 09:53:20 +00:00
|
|
|
return key, self.data[key]
|
2022-02-20 16:06:44 +00:00
|
|
|
|
2022-04-10 08:39:24 +00:00
|
|
|
def __len__(self):
|
|
|
|
return len(self.data)
|
|
|
|
|
2022-03-12 04:54:40 +00:00
|
|
|
@date_parser(1)
|
2022-02-24 02:46:45 +00:00
|
|
|
def __contains__(self, key: object) -> bool:
|
2022-04-10 08:39:24 +00:00
|
|
|
return key in self.data
|
2022-02-24 02:46:45 +00:00
|
|
|
|
2022-04-11 17:19:41 +00:00
|
|
|
def _arithmatic_validator(self, other):
|
|
|
|
"""Validates input data before performing math operatios"""
|
|
|
|
|
|
|
|
if not isinstance(other, (Number, Series, TimeSeriesCore)):
|
|
|
|
raise TypeError(
|
|
|
|
"Cannot perform mathematical operations between "
|
|
|
|
f"'{self.__class__.__name__}' and '{other.__class__.__name__}'"
|
|
|
|
)
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
if len(other) != len(self):
|
|
|
|
raise ValueError("Can only perform mathematical operations between objects of same length.")
|
|
|
|
if any(self.dates != other.dates):
|
|
|
|
raise ValueError("Can only perform mathematical operations between objects having same dates.")
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
if other.dtype != float:
|
|
|
|
raise TypeError(
|
|
|
|
"Cannot perform mathematical operations with "
|
|
|
|
f"'{other.__class__.__name__}' of type '{other.dtype}'"
|
|
|
|
)
|
|
|
|
if len(other) != len(self):
|
|
|
|
raise ValueError("Can only perform mathematical operations between objects of same length.")
|
|
|
|
|
|
|
|
def __add__(self, other):
|
|
|
|
self._arithmatic_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
other = other.values
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val + other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
elif isinstance(other, Number):
|
|
|
|
data = {dt: val + other for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
return self.__class__(data, self.frequency.symbol)
|
|
|
|
|
|
|
|
def __sub__(self, other):
|
|
|
|
self._arithmatic_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
other = other.values
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val - other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
elif isinstance(other, Number):
|
|
|
|
data = {dt: val - other for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
return self.__class__(data, self.frequency.symbol)
|
|
|
|
|
|
|
|
def __truediv__(self, other):
|
|
|
|
self._arithmatic_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
other = other.values
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val / other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
elif isinstance(other, Number):
|
|
|
|
data = {dt: val / other for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
return self.__class__(data, self.frequency.symbol)
|
|
|
|
|
|
|
|
def __floordiv__(self, other):
|
|
|
|
self._arithmatic_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
other = other.values
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val // other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
elif isinstance(other, Number):
|
|
|
|
data = {dt: val // other for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
return self.__class__(data, self.frequency.symbol)
|
|
|
|
|
|
|
|
def __mul__(self, other):
|
|
|
|
self._arithmatic_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
other = other.values
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val * other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
elif isinstance(other, Number):
|
|
|
|
data = {dt: val * other for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
return self.__class__(data, self.frequency.symbol)
|
|
|
|
|
|
|
|
def __mod__(self, other):
|
|
|
|
self._arithmatic_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
other = other.values
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val % other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
elif isinstance(other, Number):
|
|
|
|
data = {dt: val % other for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
return self.__class__(data, self.frequency.symbol)
|
|
|
|
|
|
|
|
def __pow__(self, other):
|
|
|
|
self._arithmatic_validator(other)
|
|
|
|
|
|
|
|
if isinstance(other, TimeSeriesCore):
|
|
|
|
other = other.values
|
|
|
|
|
|
|
|
if isinstance(other, Series):
|
|
|
|
data = {dt: val ** other[i] for i, (dt, val) in enumerate(self.data.items())}
|
|
|
|
elif isinstance(other, Number):
|
|
|
|
data = {dt: val**other for dt, val in self.data.items()}
|
|
|
|
|
|
|
|
return self.__class__(data, self.frequency.symbol)
|
|
|
|
|
2022-03-22 15:59:58 +00:00
|
|
|
@date_parser(1)
|
2022-04-05 05:13:53 +00:00
|
|
|
def get(self, date: str | datetime.datetime, default=None, closest=None):
|
2022-03-22 15:59:58 +00:00
|
|
|
|
|
|
|
if closest is None:
|
|
|
|
closest = FincalOptions.get_closest
|
|
|
|
|
|
|
|
if closest == "exact":
|
|
|
|
try:
|
|
|
|
item = self._get_item_from_date(date)
|
|
|
|
return item
|
|
|
|
except KeyError:
|
|
|
|
return default
|
|
|
|
|
|
|
|
if closest == "previous":
|
|
|
|
delta = datetime.timedelta(-1)
|
|
|
|
elif closest == "next":
|
|
|
|
delta = datetime.timedelta(1)
|
|
|
|
else:
|
|
|
|
raise ValueError(f"Invalid argument from closest {closest!r}")
|
|
|
|
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
item = self._get_item_from_date(date)
|
|
|
|
return item
|
|
|
|
except KeyError:
|
|
|
|
date += delta
|
|
|
|
|
2022-02-20 13:30:39 +00:00
|
|
|
@property
|
2022-03-12 04:54:40 +00:00
|
|
|
def iloc(self) -> Mapping:
|
2022-02-26 18:52:08 +00:00
|
|
|
"""Returns an item or a set of items based on index
|
|
|
|
|
|
|
|
supports slicing using numerical index.
|
|
|
|
Accepts integers or Python slice objects
|
|
|
|
|
|
|
|
Usage
|
|
|
|
-----
|
|
|
|
>>> ts = TimeSeries(data, frequency='D')
|
|
|
|
>>> ts.iloc[0] # get the first value
|
|
|
|
>>> ts.iloc[-1] # get the last value
|
|
|
|
>>> ts.iloc[:3] # get the first 3 values
|
|
|
|
>>> ts.illoc[-3:] # get the last 3 values
|
|
|
|
>>> ts.iloc[5:10] # get five values starting from the fifth value
|
|
|
|
>>> ts.iloc[::2] # get every alternate date
|
|
|
|
"""
|
2022-02-20 13:30:39 +00:00
|
|
|
|
2022-02-21 02:56:29 +00:00
|
|
|
return _IndexSlicer(self)
|
2022-03-12 04:54:40 +00:00
|
|
|
|
2022-03-29 05:05:20 +00:00
|
|
|
def head(self, n: int = 6) -> TimeSeriesCore:
|
2022-03-12 04:54:40 +00:00
|
|
|
"""Returns the first n items of the TimeSeries object"""
|
|
|
|
|
|
|
|
return self.iloc[:n]
|
|
|
|
|
2022-03-29 05:05:20 +00:00
|
|
|
def tail(self, n: int = 6) -> TimeSeriesCore:
|
2022-03-12 04:54:40 +00:00
|
|
|
"""Returns the last n items of the TimeSeries object"""
|
|
|
|
|
|
|
|
return self.iloc[-n:]
|
|
|
|
|
|
|
|
def items(self):
|
|
|
|
return self.data.items()
|