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.
- {time_split-1.1.1 → time_split-1.2.1}/LICENSE.md +1 -1
- {time_split-1.1.1 → time_split-1.2.1}/PKG-INFO +1 -2
- {time_split-1.1.1 → time_split-1.2.1}/README.md +0 -1
- {time_split-1.1.1 → time_split-1.2.1}/pyproject.toml +15 -13
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/__init__.py +1 -1
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/_limits.py +9 -7
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/_schedule.py +2 -1
- time_split-1.2.1/src/time_split/_backend/_span.py +153 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/_splitter.py +11 -15
- time_split-1.2.1/src/time_split/_compat.py +14 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_docstrings.py +1 -1
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/_plot.py +12 -9
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/_weight.py +2 -1
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/types.py +1 -1
- time_split-1.1.1/src/time_split/_backend/_span.py +0 -128
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/__main__.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/__init__.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/_datetime_index_like.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_backend/_process_available.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/__init__.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/_progress.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/_split.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_frontend/_to_string.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/_support.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/app/__init__.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/app/_create_explorer_link.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/app/reexport/__init__.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/cli.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/__init__.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/_log_progress.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/base.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/pandas/__init__.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/pandas/_impl.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/polars/__init__.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/polars/_impl.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/sklearn/__init__.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/integration/sklearn/_impl.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/py.typed +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/settings.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/src/time_split/support/__init__.py +0 -0
- {time_split-1.1.1 → time_split-1.2.1}/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.
|
|
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.
|
|
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 = "~
|
|
64
|
-
sphinx-gallery = "~0.20.0"
|
|
65
|
-
pydata-sphinx-theme = "~0.16.1"
|
|
66
|
-
myst-parser = "~
|
|
67
|
-
|
|
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.
|
|
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.
|
|
80
|
+
pytest = "~9.0.2"
|
|
79
81
|
xdoctest = "~1.3.0"
|
|
80
|
-
coverage = "~7.
|
|
82
|
+
coverage = "~7.13.1"
|
|
81
83
|
pytest-cov = "~7.0.0"
|
|
82
84
|
|
|
83
|
-
scikit-learn = "~1.
|
|
84
|
-
dask = { version = "~
|
|
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.
|
|
91
|
+
ruff = "~0.14.13"
|
|
90
92
|
|
|
91
93
|
safety = "~3.7.0"
|
|
92
94
|
codespell = "~2.4.1"
|
|
93
|
-
bump2version = "
|
|
95
|
+
bump2version = "==1.0.1"
|
|
94
96
|
|
|
95
97
|
[tool.ruff]
|
|
96
98
|
line-length = 120
|
|
@@ -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:
|
|
163
|
+
round_to: str | Timedelta,
|
|
163
164
|
tolerance: TimedeltaTypes,
|
|
164
165
|
) -> _TimedeltaTuple:
|
|
165
|
-
start_at =
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
@@ -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
|
-
|
|
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(
|
|
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)
|
|
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
|