time-split 1.1.1__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.
Files changed (39) hide show
  1. {time_split-1.1.1 → time_split-1.2.0}/PKG-INFO +1 -1
  2. {time_split-1.1.1 → time_split-1.2.0}/pyproject.toml +2 -1
  3. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/__init__.py +1 -1
  4. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_backend/_span.py +58 -34
  5. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_backend/_splitter.py +9 -13
  6. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_docstrings.py +1 -1
  7. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_frontend/_plot.py +6 -2
  8. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/types.py +1 -1
  9. {time_split-1.1.1 → time_split-1.2.0}/LICENSE.md +0 -0
  10. {time_split-1.1.1 → time_split-1.2.0}/README.md +0 -0
  11. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/__main__.py +0 -0
  12. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_backend/__init__.py +0 -0
  13. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_backend/_datetime_index_like.py +0 -0
  14. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_backend/_limits.py +0 -0
  15. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_backend/_process_available.py +0 -0
  16. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_backend/_schedule.py +0 -0
  17. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_frontend/__init__.py +0 -0
  18. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_frontend/_progress.py +0 -0
  19. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_frontend/_split.py +0 -0
  20. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_frontend/_to_string.py +0 -0
  21. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_frontend/_weight.py +0 -0
  22. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/_support.py +0 -0
  23. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/app/__init__.py +0 -0
  24. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/app/_create_explorer_link.py +0 -0
  25. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/app/reexport/__init__.py +0 -0
  26. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/cli.py +0 -0
  27. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/integration/__init__.py +0 -0
  28. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/integration/_log_progress.py +0 -0
  29. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/integration/base.py +0 -0
  30. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/integration/pandas/__init__.py +0 -0
  31. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/integration/pandas/_impl.py +0 -0
  32. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/integration/polars/__init__.py +0 -0
  33. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/integration/polars/_impl.py +0 -0
  34. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/integration/sklearn/__init__.py +0 -0
  35. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/integration/sklearn/_impl.py +0 -0
  36. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/py.typed +0 -0
  37. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/settings.py +0 -0
  38. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/support/__init__.py +0 -0
  39. {time_split-1.1.1 → time_split-1.2.0}/src/time_split/support/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: time-split
3
- Version: 1.1.1
3
+ Version: 1.2.0
4
4
  Summary: Time-based k-fold validation splits for heterogeneous data.
5
5
  License: MIT
6
6
  License-File: LICENSE.md
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "time-split"
3
- version = "1.1.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
@@ -9,4 +9,4 @@ __all__ = [
9
9
  "split",
10
10
  ]
11
11
 
12
- __version__ = "1.1.1"
12
+ __version__ = "1.2.0"
@@ -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 == "all":
52
- return "all"
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 span is False or isinstance(span, (int, float)):
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.func: FuncType
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
- if limits == NO_LIMITS:
83
- raise InvalidSpanError(
84
- span,
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.func = self._make_int(is_before, span, schedule)
90
+ self._func = self._make_int(span)
91
91
  else: # Timedelta
92
- self.func = self._make_timedelta(is_before, span, schedule)
92
+ self._func = self._make_timedelta(span)
93
93
 
94
- @staticmethod
95
- def _make_all(is_before: bool, limits: LimitsTuple) -> FuncType:
96
- retval = Timestamp(limits[0 if is_before else 1])
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
- def func(_: int) -> Timestamp | None:
99
- return retval
100
+ mid = self._schedule[i]
100
101
 
101
- return func
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
- @staticmethod
104
- def _make_int(is_before: bool, offset: int, schedule: DatetimeIndex) -> FuncType:
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 is_before else i + offset
107
- if 0 <= i < len(schedule):
108
- return schedule[i]
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
- @staticmethod
115
- def _make_timedelta(is_before: bool, offset: Timedelta, schedule: DatetimeIndex) -> FuncType:
116
- if is_before:
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 = schedule + offset
139
+ schedule = self._schedule + offset
120
140
 
121
- def func(i: int) -> Timestamp | None:
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
- def __call__(self, i: int) -> Timestamp | None:
127
- """Get start/end timestamp."""
128
- return self.func(i)
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,7 +52,7 @@ class DatetimeIndexSplitter:
52
52
  types = get_args(TimedeltaTypes)
53
53
  if (
54
54
  settings.snap_to_end
55
- and self.after != "all"
55
+ and self.after not in {"all", "empty"}
56
56
  and ms.schedule_type == "timedelta"
57
57
  and isinstance(self.after, (types, int))
58
58
  ):
@@ -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
- get_start = OffsetCalculator(
76
- self.before,
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, end = get_start(i), get_end(i)
88
- if start is None or start < min_start or start >= mid:
80
+ start = oc_start.get(i)
81
+ if start is None:
89
82
  continue
90
- if end is None or end > max_end or end <= mid:
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