From 383e11d844a99484b591c40f681f30163d4100eb Mon Sep 17 00:00:00 2001 From: Alina Voilova Date: Wed, 3 Jul 2024 13:15:28 +0200 Subject: [PATCH] added changes accroding to comments in pull_request --- portfolyo/core/pfline/decorators.py | 4 ++-- portfolyo/dev/develop.py | 6 +++--- portfolyo/tools/ceil.py | 2 +- portfolyo/tools/changefreq.py | 20 ++++++++++---------- portfolyo/tools/duration.py | 2 +- portfolyo/tools/floor.py | 2 +- portfolyo/tools/freq.py | 17 +++++++++-------- portfolyo/tools/intersect.py | 2 +- portfolyo/tools/isboundary.py | 12 ++++++------ portfolyo/tools/peakconvert.py | 6 +++--- portfolyo/tools/peakfn.py | 4 ++-- portfolyo/tools/right.py | 2 +- portfolyo/tools/round.py | 6 +++--- portfolyo/tools/standardize.py | 6 +++--- portfolyo/tools/startofday.py | 2 +- portfolyo/tools/trim.py | 4 ++-- portfolyo/tools/visualize/plot.py | 2 +- tests/tools/test_freq.py | 5 +++-- 18 files changed, 53 insertions(+), 51 deletions(-) diff --git a/portfolyo/core/pfline/decorators.py b/portfolyo/core/pfline/decorators.py index 716ba70..06e9d37 100644 --- a/portfolyo/core/pfline/decorators.py +++ b/portfolyo/core/pfline/decorators.py @@ -6,7 +6,7 @@ def assert_longest_allowed_freq(freq): def decorator(fn): def wrapped(self, *args, **kwargs): - if tools.freq.up_or_down2(self.index.freq, freq) == 1: + if tools.freq.up_or_down(self.index.freq, freq) == 1: raise ValueError( "The frequency of the index is too long; longest allowed:" f" {freq}; passed: {self.index.freq}." @@ -21,7 +21,7 @@ def wrapped(self, *args, **kwargs): def assert_shortest_allowed_freq(freq): def decorator(fn): def wrapped(self, *args, **kwargs): - if tools.freq.up_or_down2(self.index.freq, freq) == -1: + if tools.freq.up_or_down(self.index.freq, freq) == -1: raise ValueError( "The frequency of the index is too short; shortest allowed:" f" {freq}; passed: {self.index.freq}." diff --git a/portfolyo/dev/develop.py b/portfolyo/dev/develop.py index f939888..9a2e6b0 100644 --- a/portfolyo/dev/develop.py +++ b/portfolyo/dev/develop.py @@ -37,9 +37,9 @@ def get_index( if not startdate: a, m, d = 2016, 1, 1 # earliest possible a += np.random.randint(0, 8) if _seed else (periods % 8) - if tools.freq.up_or_down2(freq, "MS") <= 0: + if tools.freq.up_or_down(freq, "MS") <= 0: m += np.random.randint(0, 12) if _seed else (periods % 12) - if tools.freq.up_or_down2(freq, "D") <= 0: + if tools.freq.up_or_down(freq, "D") <= 0: d += np.random.randint(0, 28) if _seed else (periods % 28) startdate = f"{a}-{m}-{d}" if not start_of_day: @@ -48,7 +48,7 @@ def get_index( start = tools.stamp.create(startdate, tz, start_of_day) i = pd.date_range(start, periods=periods, freq=freq) # tz included in start # Some checks. - if tools.freq.up_or_down2(freq, "H") <= 0: + if tools.freq.up_or_down(freq, "H") <= 0: i = _shorten_index_if_necessary(i, start_of_day) return i diff --git a/portfolyo/tools/ceil.py b/portfolyo/tools/ceil.py index 6570ba7..a1cb631 100644 --- a/portfolyo/tools/ceil.py +++ b/portfolyo/tools/ceil.py @@ -21,7 +21,7 @@ def stamp( ---------- ts : pd.Timestamp Timestamp to ceil. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}} + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS} Frequency for which to ceil the timestamp. future : int, optional (default: 0) 0 to ceil to current period. 1 (-1) to round to period after (before) that, etc. diff --git a/portfolyo/tools/changefreq.py b/portfolyo/tools/changefreq.py index 34d6491..f63a0f8 100644 --- a/portfolyo/tools/changefreq.py +++ b/portfolyo/tools/changefreq.py @@ -48,8 +48,8 @@ def _downsample_summable(s: pd.Series, freq: str) -> pd.Series: return _emptyseries(s, freq) offset = tools_startofday.get(s.index, "timedelta") - source_vs_daily = tools_freq.up_or_down2(s.index.freq, "D") - target_vs_daily = tools_freq.up_or_down2(freq, "D") + source_vs_daily = tools_freq.up_or_down(s.index.freq, "D") + target_vs_daily = tools_freq.up_or_down(freq, "D") # We cannot simply `.resample()`, e.g. from hourly to monthly, because in that # case the start-of-day is lost. We need to do it in two steps. @@ -89,8 +89,8 @@ def _upsample_avgable(s: pd.Series, freq: str) -> pd.Series: return _emptyseries(s, freq) offset = tools_startofday.get(s.index, "timedelta") - source_vs_daily = tools_freq.up_or_down2(s.index.freq, "D") - target_vs_daily = tools_freq.up_or_down2(freq, "D") + source_vs_daily = tools_freq.up_or_down(s.index.freq, "D") + target_vs_daily = tools_freq.up_or_down(freq, "D") # Several isuses with pandas resampling: @@ -127,7 +127,7 @@ def _general(is_summable: bool, s: pd.Series, freq: str = "MS") -> pd.Series: True if data is summable, False if it is averagable. s : pd.Series Series that needs to be resampled. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}}, optional (default: 'MS') + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS}, optional (default: 'MS') Target frequency. Returns @@ -144,7 +144,7 @@ def _general(is_summable: bool, s: pd.Series, freq: str = "MS") -> pd.Series: # s is now a Series with a 'float' or 'pint' dtype. - up_or_down = tools_freq.up_or_down2(s.index.freq, freq) + up_or_down = tools_freq.up_or_down(s.index.freq, freq) # Nothing more needed; portfolio already in desired frequency. if up_or_down == 0: @@ -172,14 +172,14 @@ def index(i: pd.DatetimeIndex, freq: str = "MS") -> pd.DatetimeIndex: ---------- i : pd.DatetimeIndex Index to resample. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}} + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS} Target frequency. Returns ------- pd.DatetimeIndex """ - up_or_down = tools_freq.up_or_down2(i.freq, freq) + up_or_down = tools_freq.up_or_down(i.freq, freq) # Nothing more needed; index already in desired frequency. if up_or_down == 0: @@ -203,7 +203,7 @@ def summable(fr: Series_or_DataFrame, freq: str = "MS") -> Series_or_DataFrame: ---------- fr : Series or DataFrame Pandas Series or DataFrame to be resampled. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}}, optional (default: 'MS') + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS}, optional (default: 'MS') Target frequency. Returns @@ -239,7 +239,7 @@ def averagable(fr: Series_or_DataFrame, freq: str = "MS") -> Series_or_DataFrame ---------- fr : Series or DataFrame Pandas Series or DataFrame to be resampled. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}}, optional (default: 'MS') + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS}, optional (default: 'MS') Target frequency. Returns diff --git a/portfolyo/tools/duration.py b/portfolyo/tools/duration.py index e915e20..6d773c1 100644 --- a/portfolyo/tools/duration.py +++ b/portfolyo/tools/duration.py @@ -14,7 +14,7 @@ def stamp(ts: pd.Timestamp, freq: str) -> tools_unit.Q_: ---------- ts : pd.Timestamp Timestamp for which to calculate the duration. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}} + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS} Frequency to use in determining the duration. Returns diff --git a/portfolyo/tools/floor.py b/portfolyo/tools/floor.py index e845df5..837f64a 100644 --- a/portfolyo/tools/floor.py +++ b/portfolyo/tools/floor.py @@ -21,7 +21,7 @@ def stamp( ---------- ts : pd.Timestamp Timestamp to floor. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}} + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS} Frequency for which to floor the timestamp. future : int, optional (default: 0) 0 to floor to current period. 1 (-1) to round to period after (before) that, etc. diff --git a/portfolyo/tools/freq.py b/portfolyo/tools/freq.py index 14057e8..679cb2b 100644 --- a/portfolyo/tools/freq.py +++ b/portfolyo/tools/freq.py @@ -90,7 +90,7 @@ def assert_freq_sufficiently_long(freq, freq_ref, strict: bool = False) -> None: ) -def up_or_down2(freq_source: str, freq_target: str) -> int: +def up_or_down(freq_source: str, freq_target: str) -> int: """ Compare source frequency with target frequency to see if it needs up- or downsampling. @@ -123,18 +123,17 @@ def up_or_down2(freq_source: str, freq_target: str) -> int: ValueError """ - # check if passed freuenices are valid - # assert_freq_valid(freq_source) - # assert_freq_valid(freq_target) - # Compare if the freq are the same - if freq_source == freq_target: - return 0 restricted_classes = [ pd._libs.tslibs.offsets.QuarterBegin, pd._libs.tslibs.offsets.YearBegin, ] + # Convert freq from str to offset freq_source_as_offset = pd.tseries.frequencies.to_offset(freq_source) freq_target_as_offset = pd.tseries.frequencies.to_offset(freq_target) + + # Compare if the freq are the same + if freq_source_as_offset == freq_target_as_offset: + return 0 # One of the freq can be in restricted class, but not both if not ( type(freq_source_as_offset) in restricted_classes @@ -170,7 +169,9 @@ def up_or_down2(freq_source: str, freq_target: str) -> int: # we are in the case QS and QS return 0 - raise ValueError + raise ValueError( + f"The passed frequency {freq_source} can't be aggregated to {freq_target}." + ) def assert_freq_equally_long(freq, freq_ref) -> None: diff --git a/portfolyo/tools/intersect.py b/portfolyo/tools/intersect.py index 26c188c..c953d38 100644 --- a/portfolyo/tools/intersect.py +++ b/portfolyo/tools/intersect.py @@ -122,7 +122,7 @@ def indices_flex( if len(distinct_sod) != 1 and ignore_start_of_day is False: raise ValueError(f"Indices must have equal start-of-day; got {distinct_sod}.") for i in range(len(idxs)): - if len(distinct_sod) != 1 and tools_freq.up_or_down2(idxs[i].freq, "D") == -1: + if len(distinct_sod) != 1 and tools_freq.up_or_down(idxs[i].freq, "D") == -1: raise ValueError( "Downsample all indices to daily-or-longer, or trim them so they have the same start-of-day, before attempting to calculate the intersection" ) diff --git a/portfolyo/tools/isboundary.py b/portfolyo/tools/isboundary.py index 360bd08..1fe464a 100644 --- a/portfolyo/tools/isboundary.py +++ b/portfolyo/tools/isboundary.py @@ -88,7 +88,7 @@ def stamp(ts: pd.Timestamp, freq: str, start_of_day: dt.time = None) -> bool: ---------- ts : pd.Timestamp Timestamp for which to do the assertion. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}} + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS} Frequency for which to check if the timestamp is a valid start (or end) timestamp. start_of_day : dt.time, optional (default: midnight) Time of day at which daily-or-longer delivery periods start. E.g. if @@ -122,7 +122,7 @@ def index(i: pd.DatetimeIndex, freq: str) -> pd.Series: ---------- ts : pd.Timestamp Timestamp for which to do the assertion. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}} + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS} Frequency for which to check if the timestamp is a valid start (or end) timestamp. Returns @@ -139,17 +139,17 @@ def index(i: pd.DatetimeIndex, freq: str) -> pd.Series: until 06:00:00 (excl).) """ # When comparing index to shorter (or same) frequency, all timestamps are on boundary. - if tools_freq.up_or_down2(i.freq, freq) >= 0: + if tools_freq.up_or_down(i.freq, freq) >= 0: values = True # When comparing daily-or-longer index to other daily-or-longer frequency X, # we only need check only if the stamps are on first day of X. - elif tools_freq.up_or_down2(i.freq, "D") >= 0: + elif tools_freq.up_or_down(i.freq, "D") >= 0: values = is_X_start(i, freq) # Comparing shorter-than-daily index to other shorter-than-daily frequency X, # (i.e., '15T' with 'H') - elif tools_freq.up_or_down2(freq, "H") <= 0: + elif tools_freq.up_or_down(freq, "H") <= 0: if i.freq == "15T" and freq == "H": values = i.minute == 0 else: @@ -163,7 +163,7 @@ def index(i: pd.DatetimeIndex, freq: str) -> pd.Series: # . Check time of day. values = i.time == tools_startofday.get(i) # . Check day of X. - if tools_freq.up_or_down2(freq, "D") > 0: + if tools_freq.up_or_down(freq, "D") > 0: values &= is_X_start(i, freq) return pd.Series(values, i, name="isboundary") diff --git a/portfolyo/tools/peakconvert.py b/portfolyo/tools/peakconvert.py index 60d7b4b..7729f64 100644 --- a/portfolyo/tools/peakconvert.py +++ b/portfolyo/tools/peakconvert.py @@ -210,7 +210,7 @@ def tseries2poframe( 2020-12-01 00:00:00+01:00 57.872246 35.055449 12 rows × 3 columns """ - if tools_freq.up_or_down2(freq, "MS") < 0: + if tools_freq.up_or_down(freq, "MS") < 0: raise ValueError(f"Parameter ``freq`` be monthly-or-longer; got '{freq}'.") # Remove partial data. @@ -412,10 +412,10 @@ def poframe2poframe( 2020-07-01 00:00:00+02:00 44.033511 26.371498 2020-10-01 00:00:00+02:00 54.468722 31.063728 """ - if tools_freq.up_or_down2(freq, "MS") < 0: + if tools_freq.up_or_down(freq, "MS") < 0: raise ValueError(f"Parameter ``freq`` be monthly-or-longer; got '{freq}'.") - if tools_freq.up_or_down2(df.index.freq, freq) == 1: + if tools_freq.up_or_down(df.index.freq, freq) == 1: warnings.warn( "This conversion includes upsampling, e.g. from yearly to monthly values." " The result will be uniform at the frequency of the original frame ``df``." diff --git a/portfolyo/tools/peakfn.py b/portfolyo/tools/peakfn.py index b990e8d..7e933d7 100644 --- a/portfolyo/tools/peakfn.py +++ b/portfolyo/tools/peakfn.py @@ -107,7 +107,7 @@ def filter_time(i: pd.DatetimeIndex) -> np.ndarray: def peak_fn(i: pd.DatetimeIndex) -> pd.Series: # Check if function works for this frequency. - if tools_freq.up_or_down2(i.freq, longest_freq) > 0: + if tools_freq.up_or_down(i.freq, longest_freq) > 0: raise ValueError( f"Peak periods can only be calculated for indices with frequency of {longest_freq} or shorter." ) @@ -145,7 +145,7 @@ def peak_duration(i: pd.DatetimeIndex, peak_fn: PeakFunction) -> pd.Series: """ eval_i = i # index to evaluate if peak or offpeak for eval_freq in ("D", "H", "15T"): - if tools_freq.up_or_down2(eval_i.freq, eval_freq) > 0: # upsampling necessary + if tools_freq.up_or_down(eval_i.freq, eval_freq) > 0: # upsampling necessary eval_i = tools_changefreq.index(eval_i, eval_freq) try: eval_bool = peak_fn(eval_i) # boolean series diff --git a/portfolyo/tools/right.py b/portfolyo/tools/right.py index 9029f14..e36b1de 100644 --- a/portfolyo/tools/right.py +++ b/portfolyo/tools/right.py @@ -14,7 +14,7 @@ def stamp(ts: pd.Timestamp, freq: str = None) -> pd.Timestamp: ---------- ts : pd.Timestamp Timestamp for which to calculate the right-bound timestamp. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}} + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS} Frequency to use in determining the right-bound timestamp. Returns diff --git a/portfolyo/tools/round.py b/portfolyo/tools/round.py index 43434c9..64b59d4 100644 --- a/portfolyo/tools/round.py +++ b/portfolyo/tools/round.py @@ -22,7 +22,7 @@ def stamp_general( fn : {'floor', 'ceil'} ts : pd.Timestamp Timestamp for which to do the rounding. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}} + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS} Frequency for which to round the timestamp. future : int, optional (default: 0) 0 to round to current period. 1 (-1) to round to period after (before) that, etc. @@ -48,7 +48,7 @@ def stamp_current( fn : {'floor', 'ceil'} ts : pd.Timestamp Timestamp for which to do the rounding. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}} + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS} Frequency for which to round the timestamp. start_of_day : dt.time, optional (default: midnight) Time of day at which daily-or-longer delivery periods start. E.g. if @@ -67,7 +67,7 @@ def stamp_current( # If we land here, the timestamp is not on a boundary. # Fixed-duration frequency (= (quarter)hour): simply floor/ceil. - if tools_freq.up_or_down2(freq, "D") < 0: + if tools_freq.up_or_down(freq, "D") < 0: if fn == "floor": return ts.floor(freq, nonexistent="shift_backward") else: diff --git a/portfolyo/tools/standardize.py b/portfolyo/tools/standardize.py index 26fa99c..bc5ea1c 100644 --- a/portfolyo/tools/standardize.py +++ b/portfolyo/tools/standardize.py @@ -182,7 +182,7 @@ def assert_index_standardized(i: pd.DatetimeIndex, __right: bool = False): raise AssertionError("Index must have values; got empty index.") # Check hour and minute. - if tools_freq.up_or_down2(freq, "15T") <= 0: # quarterhour + if tools_freq.up_or_down(freq, "15T") <= 0: # quarterhour startminute = 15 if __right else 0 if i[0].minute != startminute: err = ("right-bound", "15 min past the") if __right else ("", "at a full") @@ -203,7 +203,7 @@ def assert_index_standardized(i: pd.DatetimeIndex, __right: bool = False): ) # Check time-of-day. - if tools_freq.up_or_down2(freq, "H") <= 0: # hour or shorter + if tools_freq.up_or_down(freq, "H") <= 0: # hour or shorter if not __right: start = i[0] end = tools_right.stamp(i[-1], i.freq) @@ -224,7 +224,7 @@ def assert_index_standardized(i: pd.DatetimeIndex, __right: bool = False): ) # Check day-of-X. - if tools_freq.up_or_down2(freq, "D") > 0: + if tools_freq.up_or_down(freq, "D") > 0: if freq == "MS": period, not_ok = "month", ~i.is_month_start elif freq == "QS": diff --git a/portfolyo/tools/startofday.py b/portfolyo/tools/startofday.py index 1f98cbd..50e6cf7 100644 --- a/portfolyo/tools/startofday.py +++ b/portfolyo/tools/startofday.py @@ -56,7 +56,7 @@ def set(i: pd.DatetimeIndex, start_of_day: dt.time) -> pd.DatetimeIndex: if start_of_day.second != 0 or start_of_day.minute % 15 != 0: raise ValueError("Start of day must coincide with a full quarterhour.") - if tools_freq.up_or_down2(i.freq, "D") >= 0: + if tools_freq.up_or_down(i.freq, "D") >= 0: return _set_to_longfreq(i, start_of_day) else: return _set_to_shortfreq(i, start_of_day) diff --git a/portfolyo/tools/trim.py b/portfolyo/tools/trim.py index b69e4de..932f3d9 100644 --- a/portfolyo/tools/trim.py +++ b/portfolyo/tools/trim.py @@ -20,7 +20,7 @@ def index(i: pd.DatetimeIndex, freq: str) -> pd.DatetimeIndex: ---------- i : pd.DatetimeIndex The (untrimmed) DatetimeIndex - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}} + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS} Frequency to trim to. E.g. 'MS' to only keep full months. Returns @@ -64,7 +64,7 @@ def frame(fr: pd.Series | pd.DataFrame, freq: str) -> pd.Series | pd.DataFrame: ---------- fr : Series or DataFrame The (untrimmed) pandas series or dataframe. - freq : {{{tools_freq.ALLOWED_FREQUENCIES_DOCS}}} + freq : {tools_freq.ALLOWED_FREQUENCIES_DOCS} Frequency to trim to. E.g. 'MS' to only keep full months. Returns diff --git a/portfolyo/tools/visualize/plot.py b/portfolyo/tools/visualize/plot.py index a4d61e0..09c27f4 100644 --- a/portfolyo/tools/visualize/plot.py +++ b/portfolyo/tools/visualize/plot.py @@ -209,7 +209,7 @@ def get_portfolyo_attr(ax, name, default_val=None): def is_categorical(s: pd.Series) -> bool: """The function checks whether frequency of panda Series falls into continous or categorical group""" - return tools_freq.up_or_down2(s.index.freq, "D") == 1 + return tools_freq.up_or_down(s.index.freq, "D") == 1 def prepare_ax_and_s(ax: plt.Axes, s: pd.Series, unit=None) -> pd.Series: diff --git a/tests/tools/test_freq.py b/tests/tools/test_freq.py index 483db33..88e38fb 100644 --- a/tests/tools/test_freq.py +++ b/tests/tools/test_freq.py @@ -317,6 +317,7 @@ def test_freq_sufficiently_short( ("MS", "MS", 0), ("QS", "QS", 0), ("QS", "QS-APR", 0), + ("QS", "QS-JAN", 0), # ValueError ("QS", "QS-FEB", ValueError), ("QS", "AS-FEB", ValueError), @@ -327,7 +328,7 @@ def test_freq_sufficiently_short( def test_up_pr_down2(source_freq: str, ref_freq: str, expected: int | Exception): if isinstance(expected, type) and issubclass(expected, Exception): with pytest.raises(expected): - tools.freq.up_or_down2(source_freq, ref_freq) + tools.freq.up_or_down(source_freq, ref_freq) else: - result = tools.freq.up_or_down2(source_freq, ref_freq) + result = tools.freq.up_or_down(source_freq, ref_freq) assert result == expected