time-split 1.1.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {time_split-1.1.0 → time_split-1.2.0}/PKG-INFO +1 -1
- {time_split-1.1.0 → time_split-1.2.0}/pyproject.toml +2 -1
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/__init__.py +1 -1
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_backend/_schedule.py +10 -2
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_backend/_span.py +58 -34
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_backend/_splitter.py +10 -14
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_docstrings.py +1 -1
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_frontend/_plot.py +6 -2
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/types.py +1 -1
- {time_split-1.1.0 → time_split-1.2.0}/LICENSE.md +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/README.md +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/__main__.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_backend/__init__.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_backend/_datetime_index_like.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_backend/_limits.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_backend/_process_available.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_frontend/__init__.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_frontend/_progress.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_frontend/_split.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_frontend/_to_string.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_frontend/_weight.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/_support.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/app/__init__.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/app/_create_explorer_link.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/app/reexport/__init__.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/cli.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/integration/__init__.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/integration/_log_progress.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/integration/base.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/integration/pandas/__init__.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/integration/pandas/_impl.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/integration/polars/__init__.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/integration/polars/_impl.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/integration/sklearn/__init__.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/integration/sklearn/_impl.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/py.typed +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/settings.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/support/__init__.py +0 -0
- {time_split-1.1.0 → time_split-1.2.0}/src/time_split/support/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "time-split"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "Time-based k-fold validation splits for heterogeneous data."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Richard Sundqvist", email = "richard.sundqvist@live.se" },
|
|
@@ -65,6 +65,7 @@ sphinx-gallery = "~0.20.0"
|
|
|
65
65
|
pydata-sphinx-theme = "~0.16.1"
|
|
66
66
|
myst-parser = "~4.0.1"
|
|
67
67
|
ipython = "~9.8.0"
|
|
68
|
+
sphinx-llm = "~0.2.1"
|
|
68
69
|
|
|
69
70
|
[tool.poetry.group.notebooks]
|
|
70
71
|
optional = true
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import NamedTuple, get_args
|
|
1
|
+
from typing import Literal, NamedTuple, get_args
|
|
2
2
|
|
|
3
3
|
from pandas import DatetimeIndex, NaT, Timedelta, Timestamp, date_range
|
|
4
4
|
|
|
@@ -8,6 +8,7 @@ from ._limits import LimitsTuple
|
|
|
8
8
|
from ._process_available import ProcessAvailableResult, process_available
|
|
9
9
|
|
|
10
10
|
NO_LIMITS: LimitsTuple = NaT, NaT
|
|
11
|
+
ScheduleType = Literal["cron", "explicit", "timedelta"]
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class MaterializedSchedule(NamedTuple):
|
|
@@ -15,6 +16,7 @@ class MaterializedSchedule(NamedTuple):
|
|
|
15
16
|
|
|
16
17
|
schedule: DatetimeIndex
|
|
17
18
|
available_metadata: ProcessAvailableResult
|
|
19
|
+
schedule_type: ScheduleType
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def materialize_schedule(
|
|
@@ -26,6 +28,7 @@ def materialize_schedule(
|
|
|
26
28
|
return MaterializedSchedule(
|
|
27
29
|
DatetimeIndex(schedule),
|
|
28
30
|
available_metadata=ProcessAvailableResult(None, NO_LIMITS, NO_LIMITS),
|
|
31
|
+
schedule_type="explicit",
|
|
29
32
|
)
|
|
30
33
|
except TypeError as e:
|
|
31
34
|
raise ValueError("Schedule must be explicit when not bounded by an available range.") from e
|
|
@@ -33,17 +36,22 @@ def materialize_schedule(
|
|
|
33
36
|
available_metadata = process_available(available, expand_limits=expand_limits)
|
|
34
37
|
min_dt, max_dt = available_metadata.expanded_limits
|
|
35
38
|
|
|
39
|
+
schedule_type: ScheduleType
|
|
36
40
|
if isinstance(schedule, str) and _cron_like(schedule):
|
|
37
41
|
schedule = _handle_cron(schedule, min_dt, max_dt)
|
|
42
|
+
schedule_type = "cron"
|
|
38
43
|
elif isinstance(schedule, get_args(TimedeltaTypes)):
|
|
39
44
|
schedule = _from_timedelta(schedule, available_metadata.expanded_limits)
|
|
45
|
+
schedule_type = "timedelta"
|
|
40
46
|
else:
|
|
41
47
|
if not isinstance(schedule, DatetimeIndex):
|
|
42
48
|
schedule = DatetimeIndex(schedule)
|
|
43
49
|
|
|
44
50
|
schedule = schedule.tz_localize(min_dt.tz) if schedule.tz is None else schedule.tz_convert(min_dt.tz)
|
|
45
51
|
schedule = schedule[(min_dt <= schedule) & (schedule <= max_dt)]
|
|
46
|
-
|
|
52
|
+
schedule_type = "explicit"
|
|
53
|
+
|
|
54
|
+
return MaterializedSchedule(schedule, available_metadata, schedule_type)
|
|
47
55
|
|
|
48
56
|
|
|
49
57
|
def _cron_like(schedule: str) -> bool:
|
|
@@ -8,7 +8,7 @@ from ..types import Span
|
|
|
8
8
|
from ._limits import LimitsTuple
|
|
9
9
|
from ._schedule import NO_LIMITS
|
|
10
10
|
|
|
11
|
-
StrictSpan: TypeAlias = int | Timedelta | Literal["all"]
|
|
11
|
+
StrictSpan: TypeAlias = int | Timedelta | Literal["all", "empty"]
|
|
12
12
|
SpanArgumentName = Literal["before", "after"]
|
|
13
13
|
FuncType = Callable[[int], Timestamp | None]
|
|
14
14
|
|
|
@@ -48,13 +48,13 @@ def to_strict_span(span: Span, *, name: SpanArgumentName) -> StrictSpan:
|
|
|
48
48
|
InvalidSpanError: For invalid `span` arguments.
|
|
49
49
|
|
|
50
50
|
"""
|
|
51
|
-
if span
|
|
52
|
-
return
|
|
51
|
+
if span in {"all", "empty"}:
|
|
52
|
+
return span
|
|
53
53
|
|
|
54
54
|
if isinstance(span, (str, timedelta, Timedelta)):
|
|
55
55
|
return _to_timedelta(span, name=name)
|
|
56
56
|
|
|
57
|
-
if
|
|
57
|
+
if isinstance(span, (int, float)):
|
|
58
58
|
retval = int(span)
|
|
59
59
|
if retval <= 0:
|
|
60
60
|
raise InvalidSpanError(retval, name=name)
|
|
@@ -77,52 +77,76 @@ class OffsetCalculator:
|
|
|
77
77
|
span = to_strict_span(span, name=name)
|
|
78
78
|
is_before = name == "before"
|
|
79
79
|
|
|
80
|
-
self.
|
|
80
|
+
self._schedule = schedule
|
|
81
|
+
self._is_before = is_before
|
|
82
|
+
self._limits: LimitsTuple | None = None if limits == NO_LIMITS else limits
|
|
83
|
+
|
|
84
|
+
self._func: FuncType
|
|
81
85
|
if span == "all":
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
name=name,
|
|
86
|
-
reason="requires available data to bound the schedule",
|
|
87
|
-
)
|
|
88
|
-
self.func = self._make_all(is_before, limits)
|
|
86
|
+
self._func = self.get_all
|
|
87
|
+
elif span == "empty":
|
|
88
|
+
self._func = self.get_empty
|
|
89
89
|
elif isinstance(span, int):
|
|
90
|
-
self.
|
|
90
|
+
self._func = self._make_int(span)
|
|
91
91
|
else: # Timedelta
|
|
92
|
-
self.
|
|
92
|
+
self._func = self._make_timedelta(span)
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
def get(self, i: int) -> Timestamp | None:
|
|
95
|
+
"""Get get outer bound at index `i`."""
|
|
96
|
+
rv = self._func(i)
|
|
97
|
+
if rv is None:
|
|
98
|
+
return None
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
return retval
|
|
100
|
+
mid = self._schedule[i]
|
|
100
101
|
|
|
101
|
-
|
|
102
|
+
if self._limits:
|
|
103
|
+
min_start, max_end = self._limits
|
|
104
|
+
if not (min_start <= mid <= max_end):
|
|
105
|
+
return None # Snapping to end may shift schedule out of range.
|
|
106
|
+
if not (min_start <= rv <= max_end):
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
if rv == mid and self._func != self.get_empty:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
return rv
|
|
102
113
|
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
def get_all(self, _: int) -> Timestamp:
|
|
115
|
+
if self._limits is None:
|
|
116
|
+
raise InvalidSpanError("all", name=self.name, reason="requires available data to bound the schedule")
|
|
117
|
+
|
|
118
|
+
index = 0 if self._is_before else 1
|
|
119
|
+
return self._limits[index]
|
|
120
|
+
|
|
121
|
+
def get_empty(self, i: int) -> Timestamp:
|
|
122
|
+
return self._schedule[i]
|
|
123
|
+
|
|
124
|
+
def _make_int(self, offset: int) -> FuncType:
|
|
105
125
|
def func(i: int) -> Timestamp | None:
|
|
106
|
-
i = i - offset if
|
|
107
|
-
if 0 <= i < len(
|
|
108
|
-
return
|
|
126
|
+
i = i - offset if self._is_before else i + offset
|
|
127
|
+
if 0 <= i < len(self._schedule):
|
|
128
|
+
return self._schedule[i]
|
|
109
129
|
else:
|
|
110
130
|
return None
|
|
111
131
|
|
|
132
|
+
func.__name__ = f"get_int({offset})"
|
|
112
133
|
return func
|
|
113
134
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
schedule = schedule - offset
|
|
135
|
+
def _make_timedelta(self, offset: Timedelta) -> FuncType:
|
|
136
|
+
if self._is_before:
|
|
137
|
+
schedule = self._schedule - offset
|
|
118
138
|
else:
|
|
119
|
-
schedule =
|
|
139
|
+
schedule = self._schedule + offset
|
|
120
140
|
|
|
121
|
-
def func(i: int) -> Timestamp
|
|
141
|
+
def func(i: int) -> Timestamp:
|
|
122
142
|
return schedule[i]
|
|
123
143
|
|
|
144
|
+
func.__name__ = f"get_timedelta('{offset}')"
|
|
124
145
|
return func
|
|
125
146
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return self.
|
|
147
|
+
@property
|
|
148
|
+
def name(self) -> SpanArgumentName:
|
|
149
|
+
return "before" if self._is_before else "after"
|
|
150
|
+
|
|
151
|
+
def __str__(self) -> str:
|
|
152
|
+
return f"{type(self).__name__}({self.name}={self._func.__name__})"
|
|
@@ -52,8 +52,8 @@ class DatetimeIndexSplitter:
|
|
|
52
52
|
types = get_args(TimedeltaTypes)
|
|
53
53
|
if (
|
|
54
54
|
settings.snap_to_end
|
|
55
|
-
and self.after
|
|
56
|
-
and
|
|
55
|
+
and self.after not in {"all", "empty"}
|
|
56
|
+
and ms.schedule_type == "timedelta"
|
|
57
57
|
and isinstance(self.after, (types, int))
|
|
58
58
|
):
|
|
59
59
|
ms = self._snap_to_end(ms)
|
|
@@ -72,23 +72,19 @@ class DatetimeIndexSplitter:
|
|
|
72
72
|
return ms._replace(schedule=ms.schedule + from_end)
|
|
73
73
|
|
|
74
74
|
def _make_bounds_list(self, ms: MaterializedSchedule) -> DatetimeSplits:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
ms.schedule,
|
|
78
|
-
ms.available_metadata.expanded_limits,
|
|
79
|
-
name="before",
|
|
80
|
-
)
|
|
81
|
-
get_end = OffsetCalculator(self.after, ms.schedule, ms.available_metadata.expanded_limits, name="after")
|
|
82
|
-
|
|
83
|
-
min_start, max_end = ms.available_metadata.expanded_limits
|
|
75
|
+
oc_start = OffsetCalculator(self.before, ms.schedule, ms.available_metadata.expanded_limits, name="before")
|
|
76
|
+
oc_end = OffsetCalculator(self.after, ms.schedule, ms.available_metadata.expanded_limits, name="after")
|
|
84
77
|
|
|
85
78
|
retval = []
|
|
86
79
|
for i, mid in enumerate(ms.schedule):
|
|
87
|
-
start
|
|
88
|
-
if start is None
|
|
80
|
+
start = oc_start.get(i)
|
|
81
|
+
if start is None:
|
|
89
82
|
continue
|
|
90
|
-
|
|
83
|
+
|
|
84
|
+
end = oc_end.get(i)
|
|
85
|
+
if end is None:
|
|
91
86
|
continue
|
|
87
|
+
|
|
92
88
|
retval.append(DatetimeSplitBounds(start, mid, end))
|
|
93
89
|
|
|
94
90
|
if not retval:
|
|
@@ -13,7 +13,7 @@ def docs(__func: Callable[P, T], /) -> Callable[P, T]:
|
|
|
13
13
|
offset = "pandas :ref:`offset alias <pandas:timeseries.offset_aliases>`"
|
|
14
14
|
span = (
|
|
15
15
|
f"Range {{}} `schedule` timestamps. Either a {offset}, an integer (`schedule`-based offsets), "
|
|
16
|
-
f"or `'all'` (requires `available` data)."
|
|
16
|
+
f"or `'all'` (requires `available` data). Pass `'empty'` to disable."
|
|
17
17
|
)
|
|
18
18
|
docstrings = {
|
|
19
19
|
"schedule": f"A :attr:`~time_split.types.DatetimeIterable`, {offset}, or `cron <https://pypi.org/project/croniter/>`_ expression.",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from collections.abc import Sized
|
|
2
|
+
from contextlib import suppress
|
|
2
3
|
from dataclasses import asdict, dataclass, replace
|
|
3
4
|
from typing import TYPE_CHECKING, Any, Literal
|
|
4
5
|
|
|
@@ -211,8 +212,8 @@ def _plot_splits(ax: "Axes", splits: DatetimeSplits, *, removed: set[DatetimeSpl
|
|
|
211
212
|
if fold_no == 1:
|
|
212
213
|
blue_label, red_label = settings.DATA_LABEL, settings.FUTURE_DATA_LABEL
|
|
213
214
|
|
|
214
|
-
ax.barh(i, mid - start, left=start, color="b", label=blue_label, **kwargs)
|
|
215
|
-
ax.barh(i, stop - mid, left=mid, color="r", label=red_label, **kwargs)
|
|
215
|
+
ax.barh(i, mid - start, left=start, color="b", label=None if mid == start else blue_label, **kwargs)
|
|
216
|
+
ax.barh(i, stop - mid, left=mid, color="r", label=None if mid == stop else red_label, **kwargs)
|
|
216
217
|
xtick.append(mid)
|
|
217
218
|
|
|
218
219
|
ax.set_xticks(xtick)
|
|
@@ -264,6 +265,9 @@ def _add_bar_labels(
|
|
|
264
265
|
)
|
|
265
266
|
|
|
266
267
|
for bar, label in zip(ax.containers, labels, strict=True):
|
|
268
|
+
with suppress(Exception):
|
|
269
|
+
if bar.datavalues.tolist() == [0]: # type: ignore[attr-defined]
|
|
270
|
+
continue # Zero-width bar.
|
|
267
271
|
ax.bar_label(bar, labels=[label], **kwargs) # type: ignore[arg-type]
|
|
268
272
|
|
|
269
273
|
|
|
@@ -18,7 +18,7 @@ TimedeltaTypes: _t.TypeAlias = str | _pd.Timedelta | _dt.timedelta | _np.timedel
|
|
|
18
18
|
|
|
19
19
|
Schedule: _t.TypeAlias = _pd.DatetimeIndex | DatetimeIterable | TimedeltaTypes
|
|
20
20
|
"""User schedule type."""
|
|
21
|
-
Span = int | _t.Literal["all"] | TimedeltaTypes
|
|
21
|
+
Span = int | _t.Literal["all", "empty"] | TimedeltaTypes
|
|
22
22
|
"""User span type. Used to determine limits from the timestamps given by a :attr:`Schedule`."""
|
|
23
23
|
ExpandLimits = bool | _t.Literal["auto"] | str
|
|
24
24
|
"""Limits flexibility spec for ``floor/ceil``. Pass ``False`` to disable."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|