time-split 1.1.1__tar.gz → 1.2.1__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 (41) hide show
  1. {time_split-1.1.1 → time_split-1.2.1}/LICENSE.md +1 -1
  2. {time_split-1.1.1 → time_split-1.2.1}/PKG-INFO +1 -2
  3. {time_split-1.1.1 → time_split-1.2.1}/README.md +0 -1
  4. {time_split-1.1.1 → time_split-1.2.1}/pyproject.toml +15 -13
  5. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/__init__.py +1 -1
  6. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/_limits.py +9 -7
  7. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/_schedule.py +2 -1
  8. time_split-1.2.1/src/time_split/_backend/_span.py +153 -0
  9. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/_splitter.py +11 -15
  10. time_split-1.2.1/src/time_split/_compat.py +14 -0
  11. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_docstrings.py +1 -1
  12. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/_plot.py +12 -9
  13. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/_weight.py +2 -1
  14. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/types.py +1 -1
  15. time_split-1.1.1/src/time_split/_backend/_span.py +0 -128
  16. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/__main__.py +0 -0
  17. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/__init__.py +0 -0
  18. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/_datetime_index_like.py +0 -0
  19. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/_process_available.py +0 -0
  20. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/__init__.py +0 -0
  21. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/_progress.py +0 -0
  22. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/_split.py +0 -0
  23. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/_to_string.py +0 -0
  24. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_support.py +0 -0
  25. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/app/__init__.py +0 -0
  26. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/app/_create_explorer_link.py +0 -0
  27. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/app/reexport/__init__.py +0 -0
  28. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/cli.py +0 -0
  29. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/__init__.py +0 -0
  30. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/_log_progress.py +0 -0
  31. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/base.py +0 -0
  32. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/pandas/__init__.py +0 -0
  33. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/pandas/_impl.py +0 -0
  34. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/polars/__init__.py +0 -0
  35. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/polars/_impl.py +0 -0
  36. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/sklearn/__init__.py +0 -0
  37. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/sklearn/_impl.py +0 -0
  38. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/py.typed +0 -0
  39. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/settings.py +0 -0
  40. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/support/__init__.py +0 -0
  41. {time_split-1.1.1 → time_split-1.2.1}/src/time_split/support/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright (c) 2024, Richard Sundqvist
3
+ Copyright © 2026, Richard Sundqvist
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: time-split
3
- Version: 1.1.1
3
+ Version: 1.2.1
4
4
  Summary: Time-based k-fold validation splits for heterogeneous data.
5
5
  License: MIT
6
6
  License-File: LICENSE.md
@@ -116,7 +116,6 @@ most recent stable release.
116
116
  Hosted on Read the Docs: https://time-split.readthedocs.io
117
117
 
118
118
  ## Contributing
119
-
120
119
  All contributions, bug reports, bug fixes, documentation improvements, enhancements, and ideas are welcome. To get
121
120
  started, see the [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md).
122
121
 
@@ -76,7 +76,6 @@ most recent stable release.
76
76
  Hosted on Read the Docs: https://time-split.readthedocs.io
77
77
 
78
78
  ## Contributing
79
-
80
79
  All contributions, bug reports, bug fixes, documentation improvements, enhancements, and ideas are welcome. To get
81
80
  started, see the [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md).
82
81
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "time-split"
3
- version = "1.1.1"
3
+ version = "1.2.1"
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" },
@@ -60,37 +60,39 @@ polars = { version = "~1.35.2", extras = ["pandas"] }
60
60
  # duplicate object warnings for sphinx>=4
61
61
  # description: https://github.com/astropy/astropy/issues/11723
62
62
  # https://github.com/astropy/astropy/pull/12270
63
- sphinx = "~8.1.3" # 8.2 breaks xdoc refs in README.md; maybe https://github.com/sphinx-doc/sphinx/issues/13932?
64
- sphinx-gallery = "~0.20.0"
65
- pydata-sphinx-theme = "~0.16.1"
66
- myst-parser = "~4.0.1"
67
- ipython = "~9.8.0"
63
+ sphinx = { version = "~9.1.0", python = ">=3.12" }
64
+ sphinx-gallery = { version = "~0.20.0", python = ">=3.12" }
65
+ pydata-sphinx-theme = { version = "~0.16.1", python = ">=3.12" }
66
+ myst-parser = { version = "~5.0.0", python = ">=3.12" } # https://github.com/executablebooks/MyST-Parser/issues/749
67
+ nbsphinx = { version = "~0.9.8", python = ">=3.12" }
68
+ ipython = { version = "~9.10.0", python = ">=3.12" }
69
+ sphinx-llm = { version = "~0.3.0", python = ">=3.12" }
68
70
 
69
71
  [tool.poetry.group.notebooks]
70
72
  optional = true
71
73
  [tool.poetry.group.notebooks.dependencies]
72
- jupyterlab = "~4.4.10"
74
+ jupyterlab = "~4.5.2"
73
75
  ipywidgets = "*"
74
76
  jupyterlab-execute-time = "*"
75
77
  tqdm = "*"
76
78
 
77
79
  [tool.poetry.group.test.dependencies]
78
- pytest = "~9.0.1"
80
+ pytest = "~9.0.2"
79
81
  xdoctest = "~1.3.0"
80
- coverage = "~7.12.0"
82
+ coverage = "~7.13.1"
81
83
  pytest-cov = "~7.0.0"
82
84
 
83
- scikit-learn = "~1.7.2"
84
- dask = { version = "~2025.10.0", extras = ["dataframe"] }
85
+ scikit-learn = "~1.8.0"
86
+ dask = { version = "~2026.1.1", extras = ["dataframe"] }
85
87
 
86
88
  [tool.poetry.group.devops.dependencies]
87
89
  invoke = "~2.2.1"
88
90
  mypy = "~1.19.0"
89
- ruff = "~0.14.8"
91
+ ruff = "~0.14.13"
90
92
 
91
93
  safety = "~3.7.0"
92
94
  codespell = "~2.4.1"
93
- bump2version = "~1.0.1"
95
+ bump2version = "==1.0.1"
94
96
 
95
97
  [tool.ruff]
96
98
  line-length = 120
@@ -9,4 +9,4 @@ __all__ = [
9
9
  "split",
10
10
  ]
11
11
 
12
- __version__ = "1.1.1"
12
+ __version__ = "1.2.1"
@@ -4,6 +4,7 @@ from typing import Any, NamedTuple, TypeGuard
4
4
 
5
5
  from pandas import Timedelta, Timestamp
6
6
 
7
+ from .._compat import fix_pandas4_warning, make_timedelta
7
8
  from ..settings import auto_expand_limits, misc
8
9
  from ..types import ExpandLimits, TimedeltaTypes
9
10
 
@@ -159,19 +160,20 @@ def _levels_from_settings() -> list[_TimedeltaTuple]:
159
160
 
160
161
  def _make_level(
161
162
  start_at: TimedeltaTypes | None,
162
- round_to: TimedeltaTypes,
163
+ round_to: str | Timedelta,
163
164
  tolerance: TimedeltaTypes,
164
165
  ) -> _TimedeltaTuple:
165
- start_at = Timedelta(start_at) # Will be NaT if None, making all ifs False.
166
+ start_at = make_timedelta(start_at) # Will be NaT if None, making all ifs False.
166
167
  if start_at <= Timedelta(0):
167
168
  raise ValueError(f"Bad {start_at=}; must be non-negative.")
168
169
 
169
- try:
170
- round_to = Timedelta(1, unit=round_to)
171
- except ValueError as e:
172
- raise ValueError(f"Got {round_to=}, which is not a valid frequency.") from e
170
+ if isinstance(round_to, str):
171
+ try:
172
+ round_to = Timedelta(1, unit=fix_pandas4_warning(round_to))
173
+ except ValueError as e:
174
+ raise ValueError(f"Got {round_to=}, which is not a valid frequency.") from e
173
175
 
174
- tolerance = Timedelta(tolerance)
176
+ tolerance = make_timedelta(tolerance)
175
177
  if tolerance <= Timedelta(0):
176
178
  raise ValueError(f"Bad {tolerance=}; must be non-negative.")
177
179
 
@@ -2,6 +2,7 @@ from typing import Literal, NamedTuple, get_args
2
2
 
3
3
  from pandas import DatetimeIndex, NaT, Timedelta, Timestamp, date_range
4
4
 
5
+ from .._compat import make_timedelta
5
6
  from ..settings import misc as settings
6
7
  from ..types import DatetimeIterable, ExpandLimits, Schedule, TimedeltaTypes
7
8
  from ._limits import LimitsTuple
@@ -60,7 +61,7 @@ def _cron_like(schedule: str) -> bool:
60
61
 
61
62
 
62
63
  def _from_timedelta(schedule: TimedeltaTypes, limits: LimitsTuple) -> DatetimeIndex:
63
- timedelta = Timedelta(schedule)
64
+ timedelta = make_timedelta(schedule)
64
65
  if timedelta <= Timedelta(0):
65
66
  raise ValueError(f"unbounded {schedule=} must be greater than zero.")
66
67
 
@@ -0,0 +1,153 @@
1
+ from collections.abc import Callable
2
+ from datetime import timedelta
3
+ from typing import Literal, TypeAlias
4
+
5
+ from pandas import DatetimeIndex, Timedelta, Timestamp
6
+
7
+ from .._compat import make_timedelta
8
+ from ..types import Span
9
+ from ._limits import LimitsTuple
10
+ from ._schedule import NO_LIMITS
11
+
12
+ StrictSpan: TypeAlias = int | Timedelta | Literal["all", "empty"]
13
+ SpanArgumentName = Literal["before", "after"]
14
+ FuncType = Callable[[int], Timestamp | None]
15
+
16
+
17
+ class InvalidSpanError(ValueError):
18
+ """Raised when offsets values for 'before' and 'after' arguments are invalid."""
19
+
20
+ def __init__(
21
+ self,
22
+ span: Span,
23
+ *,
24
+ name: SpanArgumentName,
25
+ reason: str = "must be greater than zero",
26
+ ) -> None:
27
+ super().__init__(f"Bad '{name}'-argument; offset {span!r} {reason}.")
28
+
29
+
30
+ def _to_timedelta(span: str | timedelta | Timedelta, *, name: SpanArgumentName) -> Timedelta:
31
+ retval = make_timedelta(span)
32
+ if retval <= Timedelta(0):
33
+ raise InvalidSpanError(span, name=name)
34
+ return retval
35
+
36
+
37
+ def to_strict_span(span: Span, *, name: SpanArgumentName) -> StrictSpan:
38
+ """Convert `span` to a strict span.
39
+
40
+ Args:
41
+ span: User span.
42
+ name: Name of user input. User for error reporting.
43
+
44
+ Returns:
45
+ A valid span with reduced types.
46
+
47
+ Raises:
48
+ TypeError: For bad `span` types.
49
+ InvalidSpanError: For invalid `span` arguments.
50
+
51
+ """
52
+ if span in {"all", "empty"}:
53
+ return span
54
+
55
+ if isinstance(span, (str, timedelta, Timedelta)):
56
+ return _to_timedelta(span, name=name)
57
+
58
+ if isinstance(span, (int, float)):
59
+ retval = int(span)
60
+ if retval <= 0:
61
+ raise InvalidSpanError(retval, name=name)
62
+ return retval
63
+
64
+ raise TypeError(f"cannot parse '{name}'-argument of type {type(span).__name__}: {span!r}.")
65
+
66
+
67
+ class OffsetCalculator:
68
+ """Utility class for computing before/after offsets from the `schedule`."""
69
+
70
+ def __init__(
71
+ self,
72
+ span: Span,
73
+ schedule: DatetimeIndex,
74
+ limits: LimitsTuple,
75
+ *,
76
+ name: SpanArgumentName,
77
+ ) -> None:
78
+ span = to_strict_span(span, name=name)
79
+ is_before = name == "before"
80
+
81
+ self._schedule = schedule
82
+ self._is_before = is_before
83
+ self._limits: LimitsTuple | None = None if limits == NO_LIMITS else limits
84
+
85
+ self._func: FuncType
86
+ if span == "all":
87
+ self._func = self.get_all
88
+ elif span == "empty":
89
+ self._func = self.get_empty
90
+ elif isinstance(span, int):
91
+ self._func = self._make_int(span)
92
+ else: # Timedelta
93
+ self._func = self._make_timedelta(span)
94
+
95
+ def get(self, i: int) -> Timestamp | None:
96
+ """Get get outer bound at index `i`."""
97
+ rv = self._func(i)
98
+ if rv is None:
99
+ return None
100
+
101
+ mid = self._schedule[i]
102
+
103
+ if self._limits:
104
+ min_start, max_end = self._limits
105
+ if not (min_start <= mid <= max_end):
106
+ return None # Snapping to end may shift schedule out of range.
107
+ if not (min_start <= rv <= max_end):
108
+ return None
109
+
110
+ if rv == mid and self._func != self.get_empty:
111
+ return None
112
+
113
+ return rv
114
+
115
+ def get_all(self, _: int) -> Timestamp:
116
+ if self._limits is None:
117
+ raise InvalidSpanError("all", name=self.name, reason="requires available data to bound the schedule")
118
+
119
+ index = 0 if self._is_before else 1
120
+ return self._limits[index]
121
+
122
+ def get_empty(self, i: int) -> Timestamp:
123
+ return self._schedule[i]
124
+
125
+ def _make_int(self, offset: int) -> FuncType:
126
+ def func(i: int) -> Timestamp | None:
127
+ i = i - offset if self._is_before else i + offset
128
+ if 0 <= i < len(self._schedule):
129
+ return self._schedule[i]
130
+ else:
131
+ return None
132
+
133
+ func.__name__ = f"get_int({offset})"
134
+ return func
135
+
136
+ def _make_timedelta(self, offset: Timedelta) -> FuncType:
137
+ if self._is_before:
138
+ schedule = self._schedule - offset
139
+ else:
140
+ schedule = self._schedule + offset
141
+
142
+ def func(i: int) -> Timestamp:
143
+ return schedule[i]
144
+
145
+ func.__name__ = f"get_timedelta('{offset}')"
146
+ return func
147
+
148
+ @property
149
+ def name(self) -> SpanArgumentName:
150
+ return "before" if self._is_before else "after"
151
+
152
+ def __str__(self) -> str:
153
+ return f"{type(self).__name__}({self.name}={self._func.__name__})"
@@ -1,9 +1,9 @@
1
1
  from dataclasses import asdict, dataclass
2
2
  from typing import cast, get_args
3
3
 
4
- from pandas import Timedelta
5
4
  from rics.misc import format_kwargs, get_by_full_name
6
5
 
6
+ from .._compat import make_timedelta
7
7
  from ..settings import misc as settings
8
8
  from ..types import (
9
9
  DatetimeIndexSplitterKwargs,
@@ -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
  ):
@@ -66,29 +66,25 @@ class DatetimeIndexSplitter:
66
66
 
67
67
  from_end = data_end - schedule_end
68
68
  if not isinstance(self.after, int):
69
- after = Timedelta(self.after)
69
+ after = make_timedelta(self.after)
70
70
  from_end -= after
71
71
 
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:
@@ -0,0 +1,14 @@
1
+ from pandas import Timedelta
2
+
3
+ from time_split.types import TimedeltaTypes
4
+
5
+
6
+ def fix_pandas4_warning(arg: str) -> str:
7
+ return arg[:-1] + "D" if arg.endswith("d") else arg
8
+
9
+
10
+ def make_timedelta(arg: TimedeltaTypes) -> Timedelta:
11
+ if isinstance(arg, str):
12
+ arg = fix_pandas4_warning(arg)
13
+
14
+ return Timedelta(arg)
@@ -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,6 @@
1
+ import os
1
2
  from collections.abc import Sized
3
+ from contextlib import suppress
2
4
  from dataclasses import asdict, dataclass, replace
3
5
  from typing import TYPE_CHECKING, Any, Literal
4
6
 
@@ -10,6 +12,7 @@ from rics.strings import format_seconds as fmt_sec
10
12
  from .._backend import DatetimeIndexSplitter
11
13
  from .._backend._datetime_index_like import DatetimeIndexLike
12
14
  from .._backend._limits import LimitsTuple
15
+ from .._compat import fix_pandas4_warning, make_timedelta
13
16
  from .._docstrings import docs
14
17
  from .._support import handle_dask
15
18
  from ..settings import plot as settings
@@ -26,11 +29,8 @@ from ._split import split
26
29
  from ._to_string import _PrettyTimestamp
27
30
  from ._weight import fold_weight
28
31
 
29
- if TYPE_CHECKING:
30
- try:
31
- from matplotlib.pyplot import Axes # type: ignore[attr-defined]
32
- except ModuleNotFoundError:
33
- Axes = Any # type: ignore[misc, assignment]
32
+ if TYPE_CHECKING or os.environ.get("SPHINX_BUILD") is True:
33
+ from matplotlib.axes import Axes # noqa: TC002
34
34
 
35
35
  Rows = Literal["rows"]
36
36
  COUNT_ROWS: Literal["rows"] = "rows"
@@ -211,8 +211,8 @@ def _plot_splits(ax: "Axes", splits: DatetimeSplits, *, removed: set[DatetimeSpl
211
211
  if fold_no == 1:
212
212
  blue_label, red_label = settings.DATA_LABEL, settings.FUTURE_DATA_LABEL
213
213
 
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)
214
+ ax.barh(i, mid - start, left=start, color="b", label=None if mid == start else blue_label, **kwargs)
215
+ ax.barh(i, stop - mid, left=mid, color="r", label=None if mid == stop else red_label, **kwargs)
216
216
  xtick.append(mid)
217
217
 
218
218
  ax.set_xticks(xtick)
@@ -232,7 +232,7 @@ def _plot_row_counts(ax: "Axes", row_count_bin: str | pd.Series, row_counts: pd.
232
232
  pretty = fmt_sec(diff(row_counts.index).min() / timedelta64(1, "s"))
233
233
  else:
234
234
  row_counts = row_counts.sort_index()
235
- pretty = fmt_sec(pd.Timedelta(row_count_bin).total_seconds())
235
+ pretty = fmt_sec(make_timedelta(row_count_bin).total_seconds())
236
236
 
237
237
  row_counts = row_counts * (max(ax.get_yticks()) / row_counts.max()) # Normalize to fold number yaxis
238
238
  ax.fill_between(
@@ -264,6 +264,9 @@ def _add_bar_labels(
264
264
  )
265
265
 
266
266
  for bar, label in zip(ax.containers, labels, strict=True):
267
+ with suppress(Exception):
268
+ if bar.datavalues.tolist() == [0]: # type: ignore[attr-defined]
269
+ continue # Zero-width bar.
267
270
  ax.bar_label(bar, labels=[label], **kwargs) # type: ignore[arg-type]
268
271
 
269
272
 
@@ -352,7 +355,7 @@ def _compute_row_counts(
352
355
  a_type = get_public_module(type(available), resolve_reexport=True, include_name=True)
353
356
  raise TypeError(f"type(available)={a_type} must have one of `floor` and `dt` to use {row_count_bin=}")
354
357
 
355
- return handle_dask(index_like.floor(row_count_bin).value_counts())
358
+ return handle_dask(index_like.floor(fix_pandas4_warning(row_count_bin)).value_counts())
356
359
 
357
360
 
358
361
  def _make_title(available: Any | None, split_kwargs: dict[str, Any]) -> str:
@@ -2,6 +2,7 @@ from typing import Literal
2
2
 
3
3
  from pandas import DatetimeIndex, Timedelta
4
4
 
5
+ from .._compat import fix_pandas4_warning
5
6
  from .._support import handle_dask
6
7
  from ..types import DatetimeIterable, DatetimeSplitCounts, DatetimeSplits
7
8
 
@@ -35,7 +36,7 @@ def fold_weight(
35
36
 
36
37
 
37
38
  def _from_unit(unit: str, splits: DatetimeSplits) -> list[DatetimeSplitCounts]:
38
- resolution = Timedelta(1, unit=unit)
39
+ resolution = Timedelta(1, unit=fix_pandas4_warning(unit))
39
40
  return [
40
41
  DatetimeSplitCounts(
41
42
  round((fold.mid - fold.start) / resolution),
@@ -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."""
@@ -1,128 +0,0 @@
1
- from collections.abc import Callable
2
- from datetime import timedelta
3
- from typing import Literal, TypeAlias
4
-
5
- from pandas import DatetimeIndex, Timedelta, Timestamp
6
-
7
- from ..types import Span
8
- from ._limits import LimitsTuple
9
- from ._schedule import NO_LIMITS
10
-
11
- StrictSpan: TypeAlias = int | Timedelta | Literal["all"]
12
- SpanArgumentName = Literal["before", "after"]
13
- FuncType = Callable[[int], Timestamp | None]
14
-
15
-
16
- class InvalidSpanError(ValueError):
17
- """Raised when offsets values for 'before' and 'after' arguments are invalid."""
18
-
19
- def __init__(
20
- self,
21
- span: Span,
22
- *,
23
- name: SpanArgumentName,
24
- reason: str = "must be greater than zero",
25
- ) -> None:
26
- super().__init__(f"Bad '{name}'-argument; offset {span!r} {reason}.")
27
-
28
-
29
- def _to_timedelta(span: str | timedelta | Timedelta, *, name: SpanArgumentName) -> Timedelta:
30
- retval = Timedelta(span)
31
- if retval <= Timedelta(0):
32
- raise InvalidSpanError(span, name=name)
33
- return retval
34
-
35
-
36
- def to_strict_span(span: Span, *, name: SpanArgumentName) -> StrictSpan:
37
- """Convert `span` to a strict span.
38
-
39
- Args:
40
- span: User span.
41
- name: Name of user input. User for error reporting.
42
-
43
- Returns:
44
- A valid span with reduced types.
45
-
46
- Raises:
47
- TypeError: For bad `span` types.
48
- InvalidSpanError: For invalid `span` arguments.
49
-
50
- """
51
- if span == "all":
52
- return "all"
53
-
54
- if isinstance(span, (str, timedelta, Timedelta)):
55
- return _to_timedelta(span, name=name)
56
-
57
- if span is False or isinstance(span, (int, float)):
58
- retval = int(span)
59
- if retval <= 0:
60
- raise InvalidSpanError(retval, name=name)
61
- return retval
62
-
63
- raise TypeError(f"cannot parse '{name}'-argument of type {type(span).__name__}: {span!r}.")
64
-
65
-
66
- class OffsetCalculator:
67
- """Utility class for computing before/after offsets from the `schedule`."""
68
-
69
- def __init__(
70
- self,
71
- span: Span,
72
- schedule: DatetimeIndex,
73
- limits: LimitsTuple,
74
- *,
75
- name: SpanArgumentName,
76
- ) -> None:
77
- span = to_strict_span(span, name=name)
78
- is_before = name == "before"
79
-
80
- self.func: FuncType
81
- 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)
89
- elif isinstance(span, int):
90
- self.func = self._make_int(is_before, span, schedule)
91
- else: # Timedelta
92
- self.func = self._make_timedelta(is_before, span, schedule)
93
-
94
- @staticmethod
95
- def _make_all(is_before: bool, limits: LimitsTuple) -> FuncType:
96
- retval = Timestamp(limits[0 if is_before else 1])
97
-
98
- def func(_: int) -> Timestamp | None:
99
- return retval
100
-
101
- return func
102
-
103
- @staticmethod
104
- def _make_int(is_before: bool, offset: int, schedule: DatetimeIndex) -> FuncType:
105
- 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]
109
- else:
110
- return None
111
-
112
- return func
113
-
114
- @staticmethod
115
- def _make_timedelta(is_before: bool, offset: Timedelta, schedule: DatetimeIndex) -> FuncType:
116
- if is_before:
117
- schedule = schedule - offset
118
- else:
119
- schedule = schedule + offset
120
-
121
- def func(i: int) -> Timestamp | None:
122
- return schedule[i]
123
-
124
- return func
125
-
126
- def __call__(self, i: int) -> Timestamp | None:
127
- """Get start/end timestamp."""
128
- return self.func(i)