time-split 0.2.1__tar.gz → 0.3.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-0.2.1 → time_split-0.3.0}/PKG-INFO +1 -1
- {time_split-0.2.1 → time_split-0.3.0}/pyproject.toml +6 -6
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/__init__.py +1 -1
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_frontend/__init__.py +2 -1
- time_split-0.3.0/src/time_split/_frontend/_progress.py +244 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/_log_progress.py +4 -4
- time_split-0.2.1/src/time_split/integration/split_data.py → time_split-0.3.0/src/time_split/integration/base.py +1 -1
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/pandas/_impl.py +3 -3
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/polars/_impl.py +3 -3
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/sklearn/_impl.py +2 -1
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/settings.py +6 -4
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/support/__init__.py +2 -1
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/types.py +39 -0
- time_split-0.2.1/src/time_split/_frontend/_progress.py +0 -125
- {time_split-0.2.1 → time_split-0.3.0}/LICENSE.md +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/README.md +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/__init__.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_datetime_index_like.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_limits.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_process_available.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_schedule.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_span.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_backend/_splitter.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_docstrings.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_frontend/_plot.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_frontend/_split.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_frontend/_to_string.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_frontend/_weight.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/_support.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/__init__.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/pandas/__init__.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/polars/__init__.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/integration/sklearn/__init__.py +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/py.typed +0 -0
- {time_split-0.2.1 → time_split-0.3.0}/src/time_split/support/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "time-split"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Time-based k-fold validation splits for heterogeneous data."
|
|
5
5
|
authors = ["Richard Sundqvist <richard.sundqvist@live.se>"]
|
|
6
6
|
|
|
@@ -46,7 +46,7 @@ plotting = ["matplotlib", "seaborn"]
|
|
|
46
46
|
[tool.poetry.group.manual-extras.dependencies]
|
|
47
47
|
# Extras that must be available for docs + tests go here. Doc/test-only deps go in their respective groups.
|
|
48
48
|
croniter = "^2.0.3"
|
|
49
|
-
pyarrow = "
|
|
49
|
+
pyarrow = ">=15.0.2,<17.0.0"
|
|
50
50
|
polars = { version = "^0.20.19", extras = ["pandas"] }
|
|
51
51
|
|
|
52
52
|
[tool.poetry.group.docs]
|
|
@@ -55,9 +55,9 @@ polars = { version = "^0.20.19", extras = ["pandas"] }
|
|
|
55
55
|
# description: https://github.com/astropy/astropy/issues/11723
|
|
56
56
|
# https://github.com/astropy/astropy/pull/12270
|
|
57
57
|
sphinx = "^7.2.6"
|
|
58
|
-
sphinx-gallery = "^0.
|
|
58
|
+
sphinx-gallery = "^0.16.0"
|
|
59
59
|
pydata-sphinx-theme = "^0.15.2"
|
|
60
|
-
myst-parser = "
|
|
60
|
+
myst-parser = ">=2,<4"
|
|
61
61
|
nbsphinx = "^0.9.3"
|
|
62
62
|
ipython = "^8.23.0"
|
|
63
63
|
|
|
@@ -80,12 +80,12 @@ scikit-learn = "^1.4.2"
|
|
|
80
80
|
# dask.datasets.timeseries() warns:
|
|
81
81
|
# UserWarning: dask_expr does not support the DataFrameIOFunction protocol for column projection
|
|
82
82
|
# on dask == 2024.3.1
|
|
83
|
-
dask = { version = "==2024.
|
|
83
|
+
dask = { version = "==2024.5.0", extras = ["dataframe"] }
|
|
84
84
|
|
|
85
85
|
[tool.poetry.group.devops.dependencies]
|
|
86
86
|
invoke = "^2.2.0"
|
|
87
87
|
mypy = "^1.9.0"
|
|
88
|
-
ruff = "
|
|
88
|
+
ruff = ">=0.3.7,<0.5.0"
|
|
89
89
|
|
|
90
90
|
safety = "^3.1.0"
|
|
91
91
|
codespell = "^2.2.6"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from ._plot import plot
|
|
2
|
-
from ._progress import log_split_progress
|
|
2
|
+
from ._progress import default_metrics_formatter, log_split_progress
|
|
3
3
|
from ._split import split
|
|
4
4
|
from ._to_string import to_string
|
|
5
5
|
from ._weight import fold_weight
|
|
@@ -7,6 +7,7 @@ from ._weight import fold_weight
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"fold_weight",
|
|
9
9
|
"log_split_progress",
|
|
10
|
+
"default_metrics_formatter",
|
|
10
11
|
"plot",
|
|
11
12
|
"split",
|
|
12
13
|
"to_string",
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Callable, Iterable, MutableMapping, Sequence
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from time import perf_counter
|
|
5
|
+
from typing import Any, Generic
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from rics.collections.dicts import flatten_dict
|
|
9
|
+
from rics.performance import format_seconds
|
|
10
|
+
|
|
11
|
+
from ..settings import log_split_progress as settings
|
|
12
|
+
from ..types import DatetimeSplitBounds, FormatMetrics, GetMetrics, LoggerArg, MetricsType, SplitProgressExtras
|
|
13
|
+
from ._to_string import _PrettyTimestamp
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def log_split_progress(
|
|
17
|
+
splits: Sequence[DatetimeSplitBounds],
|
|
18
|
+
*,
|
|
19
|
+
logger: LoggerArg = "time_split",
|
|
20
|
+
start_level: int = logging.INFO,
|
|
21
|
+
end_level: int = logging.INFO,
|
|
22
|
+
extra: dict[str, Any] | None = None,
|
|
23
|
+
get_metrics: GetMetrics[MetricsType] | None = None,
|
|
24
|
+
) -> Iterable[DatetimeSplitBounds]:
|
|
25
|
+
"""Log iteration progress.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
splits: Splits to iterate over.
|
|
29
|
+
logger: Logger or logger name to use.
|
|
30
|
+
start_level: Log level to use for the :attr:`fold-begin message <.settings.log_split_progress.START_MESSAGE>`.
|
|
31
|
+
end_level: Log level to use for the :attr:`fold-end message <.settings.log_split_progress.END_MESSAGE>`.
|
|
32
|
+
extra: Immutable, user-defined `extra`-arguments to use when logging, merged with progress-related extras (see
|
|
33
|
+
:class:`~time_split.types.SplitProgressExtras`).
|
|
34
|
+
get_metrics: A callable ``(training_date) -> fold_metrics | str`` (see :attr:`~.DatetimeSplit.training_date`).
|
|
35
|
+
If given, metrics are added to the :attr:`fold-end message <.settings.log_split_progress.END_MESSAGE>`. The
|
|
36
|
+
message is formatted using the :func:`default formatter <.support.default_metrics_formatter>` unless
|
|
37
|
+
:attr:`~.settings.log_split_progress.FORMAT_METRICS` is set. If this callback returns a ``str`` argument,
|
|
38
|
+
the :func:`default formatter <.support.default_metrics_formatter>` will assume that the metrics are
|
|
39
|
+
pre-formatted, simply appending the formatted metrics to the
|
|
40
|
+
:attr:`fold-end message <.settings.log_split_progress.END_MESSAGE>` as-is.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
An iterable over `splits`.
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
Configuring the `logger` name and
|
|
47
|
+
:attr:`fold-begin message <.settings.log_split_progress.START_MESSAGE>` log level.
|
|
48
|
+
|
|
49
|
+
>>> from time_split import split, log_split_progress
|
|
50
|
+
>>> schedule = ["2023-08-16", "2023-08-17 12", "2023-08-19"]
|
|
51
|
+
>>> tracked_splits = log_split_progress(
|
|
52
|
+
... split(schedule),
|
|
53
|
+
... logger="progress",
|
|
54
|
+
... start_level=logging.DEBUG,
|
|
55
|
+
... )
|
|
56
|
+
>>> list(splits) # doctest: +SKIP
|
|
57
|
+
[progress:DEBUG] Begin fold 1/2: '2023-08-09' <= [schedule: '2023-08-16' (Wednesday)] < '2023-08-17 12:00:00'.
|
|
58
|
+
[progress:INFO] Finished fold 1/2 [schedule: '2023-08-16' (Wednesday)] after 5m 18s.
|
|
59
|
+
[progress:DEBUG] Begin fold 2/2: '2023-08-10 12:00:00' <= [schedule: '2023-08-17 12:00:00' (Thursday)] < '2023-08-19'.
|
|
60
|
+
[progress:INFO] Finished fold 2/2 [schedule: '2023-08-17 12:00:00' (Thursday)] after 4m 3s.
|
|
61
|
+
|
|
62
|
+
Using the `get_metrics` callback argument.
|
|
63
|
+
|
|
64
|
+
>>> metrics = {
|
|
65
|
+
... "2023-08-16 00:00:00": {"rmse": {"train": 0.11, "test": 0.5}},
|
|
66
|
+
... "2023-08-17 12:00:00": {"rmse": {"test": 0.5, "future": 20.19}},
|
|
67
|
+
... }
|
|
68
|
+
>>> tracked_splits = log_split_progress(
|
|
69
|
+
... split(schedule),
|
|
70
|
+
... get_metrics=lambda key: metrics[str(key)],
|
|
71
|
+
... )
|
|
72
|
+
>>> list(tracked_splits) # doctest: +SKIP
|
|
73
|
+
[time_split:INFO] Begin fold 1/2: '2023-08-09' <= [schedule: '2023-08-16' (Wednesday)] < '2023-08-17 12:00:00'.
|
|
74
|
+
[time_split:INFO] Finished fold 1/2 [schedule: '2023-08-16' (Wednesday)] after 5m 18s. Fold metrics:
|
|
75
|
+
rmse.train 0.11
|
|
76
|
+
rmse.test 0.5
|
|
77
|
+
[time_split:INFO] Begin fold 2/2: '2023-08-10 12:00:00' <= [schedule: '2023-08-17 12:00:00' (Thursday)] < '2023-08-19'.
|
|
78
|
+
[time_split:INFO] Finished fold 2/2 [schedule: '2023-08-17 12:00:00' (Thursday)] after 4m 3s. Fold metrics:
|
|
79
|
+
rmse.test 0.5
|
|
80
|
+
rmse.future 20.19
|
|
81
|
+
|
|
82
|
+
Formatting was done using the :func:`default formatter <.support.default_metrics_formatter>`, since the
|
|
83
|
+
:attr:`~.settings.log_split_progress.FORMAT_METRICS` setting is ``None``.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
logger = logging.getLogger(logger) if isinstance(logger, str) else logger
|
|
87
|
+
|
|
88
|
+
if isinstance(logger, logging.LoggerAdapter) and not hasattr(logger, "merge_extra"):
|
|
89
|
+
# Backport of https://github.com/python/cpython/pull/107292
|
|
90
|
+
logger = _MergingLoggerAdapter(logger.logger, logger.extra)
|
|
91
|
+
|
|
92
|
+
track = _ProgressTracker(
|
|
93
|
+
logger=logger,
|
|
94
|
+
fold_format=settings.FOLD_FORMAT,
|
|
95
|
+
start_level=start_level,
|
|
96
|
+
start_message=settings.START_MESSAGE,
|
|
97
|
+
end_level=end_level,
|
|
98
|
+
end_message=settings.END_MESSAGE,
|
|
99
|
+
seconds_formatter=settings.SECONDS_FORMATTER or format_seconds,
|
|
100
|
+
user_extra={} if extra is None else extra.copy(), # Not actually immutable; deepcopy can be very expensive.
|
|
101
|
+
get_metrics=get_metrics,
|
|
102
|
+
format_metrics=settings.FORMAT_METRICS or default_metrics_formatter,
|
|
103
|
+
)
|
|
104
|
+
return track(splits)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class _ProgressTracker(Generic[MetricsType]):
|
|
109
|
+
logger: logging.Logger | logging.LoggerAdapter # type: ignore[type-arg]
|
|
110
|
+
fold_format: str
|
|
111
|
+
start_level: int
|
|
112
|
+
start_message: str
|
|
113
|
+
end_level: int
|
|
114
|
+
end_message: str
|
|
115
|
+
seconds_formatter: Callable[[float], str]
|
|
116
|
+
user_extra: dict[str, Any]
|
|
117
|
+
get_metrics: GetMetrics[MetricsType] | None
|
|
118
|
+
format_metrics: FormatMetrics[MetricsType]
|
|
119
|
+
|
|
120
|
+
def __call__(self, splits: Sequence[DatetimeSplitBounds]) -> Iterable[DatetimeSplitBounds]:
|
|
121
|
+
n_splits = len(splits)
|
|
122
|
+
|
|
123
|
+
for n, split in enumerate(splits, start=1):
|
|
124
|
+
default_extras = SplitProgressExtras(
|
|
125
|
+
n=n,
|
|
126
|
+
n_splits=n_splits,
|
|
127
|
+
start=split.start.isoformat(),
|
|
128
|
+
mid=split.mid.isoformat(),
|
|
129
|
+
end=split.end.isoformat(),
|
|
130
|
+
)
|
|
131
|
+
extra = {**self.user_extra, **default_extras}
|
|
132
|
+
kwargs: dict[str, Any] = dict(
|
|
133
|
+
n=n,
|
|
134
|
+
n_splits=n_splits,
|
|
135
|
+
start=_PrettyTimestamp(split.start),
|
|
136
|
+
mid=_PrettyTimestamp(split.mid),
|
|
137
|
+
end=_PrettyTimestamp(split.end),
|
|
138
|
+
**self.user_extra,
|
|
139
|
+
)
|
|
140
|
+
kwargs.update(fold=self.fold_format.format(**kwargs))
|
|
141
|
+
|
|
142
|
+
self.logger.log(self.start_level, self.start_message.format(**kwargs), extra=extra)
|
|
143
|
+
|
|
144
|
+
# Yield split and count user time.
|
|
145
|
+
start = perf_counter()
|
|
146
|
+
yield split
|
|
147
|
+
seconds = round(perf_counter() - start, 6)
|
|
148
|
+
|
|
149
|
+
kwargs.update(
|
|
150
|
+
seconds=seconds,
|
|
151
|
+
formatted_seconds=self.seconds_formatter(seconds),
|
|
152
|
+
)
|
|
153
|
+
msg = self.end_message.format(**kwargs)
|
|
154
|
+
|
|
155
|
+
if self.get_metrics is not None:
|
|
156
|
+
extra["metrics"] = self.get_metrics(split.mid)
|
|
157
|
+
msg = self.format_metrics(msg, extra["metrics"])
|
|
158
|
+
|
|
159
|
+
extra.update(seconds=seconds)
|
|
160
|
+
self.logger.log(self.end_level, msg, extra=extra)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def default_metrics_formatter(end_message: str, metrics: dict[Any, Any] | pd.Series | pd.DataFrame | str | Any) -> str:
|
|
164
|
+
"""Default formatting implementation.
|
|
165
|
+
|
|
166
|
+
Format using an appropriate pandas ``to_string()``-method if `metrics` is a ``dict`` or a pandas type. Nested
|
|
167
|
+
dictionaries are flattened using :func:`~rics.collections.dicts.flatten_dict` if `metrics` is a dict-of-dicts.
|
|
168
|
+
|
|
169
|
+
Metrics of type ``str`` are assumed to be preformatted, and are appended to `end_message` as-is.
|
|
170
|
+
|
|
171
|
+
If any other types are given, fall back to
|
|
172
|
+
``f"{end_message} Metrics: {metrics}"``.
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
Formatting a nested dict.
|
|
176
|
+
|
|
177
|
+
>>> metrics = {"rmse": {"train": 0.11, "test": 0.5, "future": 20.19}}
|
|
178
|
+
|
|
179
|
+
>>> print(default_metrics_formatter("End message.", metrics))
|
|
180
|
+
End message. Fold metrics:
|
|
181
|
+
train test future
|
|
182
|
+
rmse 0.11 0.5 20.19
|
|
183
|
+
|
|
184
|
+
Formatting a :class:`pandas.DataFrame`.
|
|
185
|
+
|
|
186
|
+
>>> metrics = {"me": [0.1, 0.2, 0.3], "rmse": [0.11, 0.5, 20.19]}
|
|
187
|
+
>>> df = pd.DataFrame(metrics, index=["train", "test", "future"])
|
|
188
|
+
>>> print(default_metrics_formatter("End message.", df))
|
|
189
|
+
End message. Fold metrics:
|
|
190
|
+
me rmse
|
|
191
|
+
train 0.1 0.11
|
|
192
|
+
test 0.2 0.50
|
|
193
|
+
future 0.3 20.19
|
|
194
|
+
|
|
195
|
+
The index printed unless` it is a :class:`pandas.RangeIndex`.
|
|
196
|
+
"""
|
|
197
|
+
if isinstance(metrics, dict):
|
|
198
|
+
metrics = _convert_dict(metrics)
|
|
199
|
+
|
|
200
|
+
if isinstance(metrics, (pd.Series, pd.DataFrame)):
|
|
201
|
+
if isinstance(metrics, pd.DataFrame):
|
|
202
|
+
with_index = not isinstance(metrics, pd.RangeIndex)
|
|
203
|
+
else:
|
|
204
|
+
with_index = True
|
|
205
|
+
return f"{end_message} Fold metrics:\n{metrics.round(3).to_string(index=with_index)}"
|
|
206
|
+
elif isinstance(metrics, str):
|
|
207
|
+
return f"{end_message} {metrics}"
|
|
208
|
+
else:
|
|
209
|
+
return f"{end_message} Metrics: {metrics}"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class _MergingLoggerAdapter(logging.LoggerAdapter[Any]):
|
|
213
|
+
# TODO(3.13): Use merge_extra=True init arg
|
|
214
|
+
def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> tuple[Any, MutableMapping[str, Any]]:
|
|
215
|
+
"""See https://github.com/python/cpython/pull/107292."""
|
|
216
|
+
kwargs["extra"] = {**self.extra, **kwargs["extra"]} if "extra" in kwargs and self.extra else self.extra
|
|
217
|
+
return msg, kwargs
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _convert_dict(metrics: dict[Any, Any]) -> dict[Any, Any] | pd.DataFrame | pd.Series:
|
|
221
|
+
original = metrics
|
|
222
|
+
|
|
223
|
+
scalar_leaves: bool = False
|
|
224
|
+
|
|
225
|
+
if all(isinstance(v, dict) for v in metrics.values()):
|
|
226
|
+
flat = flatten_dict(metrics)
|
|
227
|
+
|
|
228
|
+
# A single dot usually indicates nesting one level deep; can be formatted as a DataFrame.
|
|
229
|
+
if any(k.count(".") != 1 for k in flat) and all(pd.api.types.is_scalar(v) for v in flat.values()):
|
|
230
|
+
metrics = flat
|
|
231
|
+
elif all(pd.api.types.is_scalar(v) for v in original.values()):
|
|
232
|
+
scalar_leaves = True
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if scalar_leaves:
|
|
236
|
+
return pd.Series(metrics)
|
|
237
|
+
|
|
238
|
+
df = pd.DataFrame(metrics)
|
|
239
|
+
except Exception:
|
|
240
|
+
return original
|
|
241
|
+
|
|
242
|
+
if len(df) > len(df.columns):
|
|
243
|
+
df = df.T # Prefer wider horizontal output
|
|
244
|
+
return df
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from collections.abc import Iterable
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
from .. import log_split_progress
|
|
6
6
|
from .._docstrings import add_docstrings
|
|
7
|
-
from ..types import DatetimeSplitBounds, DatetimeSplits
|
|
7
|
+
from ..types import DatetimeSplitBounds, DatetimeSplits, LogSplitProgressKwargs, MetricsType
|
|
8
8
|
|
|
9
|
-
LogProgressArg =
|
|
9
|
+
LogProgressArg = str | bool | logging.Logger | logging.LoggerAdapter[Any] | LogSplitProgressKwargs[MetricsType]
|
|
10
10
|
|
|
11
11
|
add_docstrings(log_progress="Controls logging of fold progress. See :func:`~.log_split_progress` for details.")
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def handle_log_progress_arg(
|
|
15
|
-
log_progress: LogProgressArg, *, splits: DatetimeSplits
|
|
15
|
+
log_progress: LogProgressArg[MetricsType], *, splits: DatetimeSplits
|
|
16
16
|
) -> Iterable[DatetimeSplitBounds] | None:
|
|
17
17
|
"""Wrapper function for integrations."""
|
|
18
18
|
if log_progress is True:
|
|
@@ -49,7 +49,7 @@ class DatetimeSplit(_t.NamedTuple, _t.Generic[DataT]):
|
|
|
49
49
|
def split_data(
|
|
50
50
|
data: DataT,
|
|
51
51
|
*,
|
|
52
|
-
log_progress: _log_progress.LogProgressArg = False,
|
|
52
|
+
log_progress: _log_progress.LogProgressArg[_tst.MetricsType] = False,
|
|
53
53
|
as_available: DataAsAvailableFn[DataT],
|
|
54
54
|
select: DataSelectFn[DataT],
|
|
55
55
|
**kwargs: _t.Unpack[_tst.DatetimeIndexSplitterKwargs],
|
|
@@ -6,9 +6,9 @@ from pandas import DataFrame, DatetimeIndex, Series, Timestamp
|
|
|
6
6
|
from rics.misc import tname
|
|
7
7
|
|
|
8
8
|
from ..._docstrings import docs
|
|
9
|
-
from ...types import DatetimeIndexSplitterKwargs
|
|
9
|
+
from ...types import DatetimeIndexSplitterKwargs, MetricsType
|
|
10
10
|
from .._log_progress import LogProgressArg
|
|
11
|
-
from ..
|
|
11
|
+
from ..base import DatetimeSplit, split_data
|
|
12
12
|
|
|
13
13
|
PandasT = TypeVar("PandasT", Series, DataFrame)
|
|
14
14
|
"""A splittable pandas type."""
|
|
@@ -19,7 +19,7 @@ def split_pandas(
|
|
|
19
19
|
data: PandasT,
|
|
20
20
|
time_column: Hashable = None,
|
|
21
21
|
*,
|
|
22
|
-
log_progress: LogProgressArg = False,
|
|
22
|
+
log_progress: LogProgressArg[MetricsType] = False,
|
|
23
23
|
**kwargs: Unpack[DatetimeIndexSplitterKwargs],
|
|
24
24
|
) -> Iterable[DatetimeSplit[PandasT]]:
|
|
25
25
|
"""Split a pandas type.
|
|
@@ -6,9 +6,9 @@ from polars import DataFrame, Series
|
|
|
6
6
|
from rics.misc import tname
|
|
7
7
|
|
|
8
8
|
from ..._docstrings import docs
|
|
9
|
-
from ...types import DatetimeIndexSplitterKwargs
|
|
9
|
+
from ...types import DatetimeIndexSplitterKwargs, MetricsType
|
|
10
10
|
from .._log_progress import LogProgressArg
|
|
11
|
-
from ..
|
|
11
|
+
from ..base import DatetimeSplit, split_data
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@docs
|
|
@@ -16,7 +16,7 @@ def split_polars(
|
|
|
16
16
|
data: DataFrame,
|
|
17
17
|
time_column: str,
|
|
18
18
|
*,
|
|
19
|
-
log_progress: LogProgressArg = False,
|
|
19
|
+
log_progress: LogProgressArg[MetricsType] = False,
|
|
20
20
|
**kwargs: Unpack[DatetimeIndexSplitterKwargs],
|
|
21
21
|
) -> Iterable[DatetimeSplit[DataFrame]]:
|
|
22
22
|
"""Split a polars frame.
|
|
@@ -11,6 +11,7 @@ from ...types import (
|
|
|
11
11
|
DatetimeSplits,
|
|
12
12
|
DatetimeTypes,
|
|
13
13
|
Flex,
|
|
14
|
+
MetricsType,
|
|
14
15
|
Schedule,
|
|
15
16
|
Span,
|
|
16
17
|
)
|
|
@@ -60,7 +61,7 @@ class ScikitLearnSplitter(BaseCrossValidator): # type: ignore[misc]
|
|
|
60
61
|
n_splits: int = 0,
|
|
61
62
|
flex: Flex = "auto",
|
|
62
63
|
step: int = 1,
|
|
63
|
-
log_progress: LogProgressArg = False,
|
|
64
|
+
log_progress: LogProgressArg[MetricsType] = False,
|
|
64
65
|
verify_xy: bool = True,
|
|
65
66
|
) -> None:
|
|
66
67
|
super().__init__()
|
|
@@ -113,11 +113,10 @@ class log_split_progress: # noqa: N801
|
|
|
113
113
|
'2021-12-30' <= [schedule: '2022-01-04' (Tuesday)] < '2022-01-04 18:00:00'
|
|
114
114
|
"""
|
|
115
115
|
|
|
116
|
-
SECONDS_FORMATTER:
|
|
117
|
-
"""A callable ``(seconds) -> formatted_seconds``.
|
|
116
|
+
SECONDS_FORMATTER: _t.Callable[[float], str] | None = None
|
|
117
|
+
"""A callable ``(seconds) -> formatted_seconds``. Use :func:`rics.performance.format_seconds` if ``None``.
|
|
118
118
|
|
|
119
|
-
Both ``seconds`` and ``formatted_seconds`` will be available to the :attr:`END_MESSAGE` message.
|
|
120
|
-
given, the actual callable will be resolved using :func:`rics.misc.get_by_full_name`.
|
|
119
|
+
Both ``seconds`` and ``formatted_seconds`` will be available to the :attr:`END_MESSAGE` message.
|
|
121
120
|
"""
|
|
122
121
|
|
|
123
122
|
START_MESSAGE: str = "Begin fold {n}/{n_splits}: {fold}."
|
|
@@ -156,6 +155,9 @@ class log_split_progress: # noqa: N801
|
|
|
156
155
|
AUTO_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
157
156
|
"""Long-form timestamp format_spec used by ``<key>.auto``."""
|
|
158
157
|
|
|
158
|
+
FORMAT_METRICS: _tst.FormatMetrics[_t.Any] | None = None
|
|
159
|
+
"""A callable ``(end_message, metrics) -> str``. Use the :func:`default formatter <.support.default_metrics_formatter>` if ``None``."""
|
|
160
|
+
|
|
159
161
|
|
|
160
162
|
class misc: # noqa: N801
|
|
161
163
|
"""Miscellaneous settings that don't fit in any other namespace."""
|
|
@@ -12,12 +12,13 @@ dependencies if you need to use the ``support`` module.
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from .._backend import DatetimeIndexSplitter, expand_limits, process_available
|
|
15
|
-
from .._frontend import fold_weight, to_string
|
|
15
|
+
from .._frontend import default_metrics_formatter, fold_weight, to_string
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
18
|
"DatetimeIndexSplitter",
|
|
19
19
|
"expand_limits",
|
|
20
20
|
"process_available",
|
|
21
|
+
"default_metrics_formatter",
|
|
21
22
|
"fold_weight",
|
|
22
23
|
"to_string",
|
|
23
24
|
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Types related to splitting data."""
|
|
2
2
|
|
|
3
3
|
import datetime as _dt
|
|
4
|
+
import logging as _logging
|
|
4
5
|
import typing as _t
|
|
5
6
|
|
|
6
7
|
import numpy as _np
|
|
@@ -62,3 +63,41 @@ class DatetimeIndexSplitterKwargs(_t.TypedDict, total=False):
|
|
|
62
63
|
step: int
|
|
63
64
|
n_splits: int
|
|
64
65
|
flex: Flex
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
LoggerArg = _logging.Logger | _logging.LoggerAdapter[_t.Any] | str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
MetricsType = _t.TypeVar("MetricsType")
|
|
72
|
+
"""Metrics argument type."""
|
|
73
|
+
GetMetrics = _t.Callable[[_pd.Timestamp], MetricsType]
|
|
74
|
+
"""A callable ``(Timestamp) -> MetricsType``."""
|
|
75
|
+
FormatMetrics = _t.Callable[[str, MetricsType], str]
|
|
76
|
+
"""A callable ``(end_message, metrics) -> str``."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class LogSplitProgressKwargs(_t.TypedDict, _t.Generic[MetricsType], total=False):
|
|
80
|
+
"""Keyword arguments for by :func:`.log_split_progress`."""
|
|
81
|
+
|
|
82
|
+
logger: LoggerArg
|
|
83
|
+
start_level: int
|
|
84
|
+
end_level: int
|
|
85
|
+
extra: dict[str, _t.Any] | None
|
|
86
|
+
get_metrics: GetMetrics[MetricsType]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SplitProgressExtras(_t.TypedDict):
|
|
90
|
+
"""Named `extras` used for messages logged by :func:`.log_split_progress`."""
|
|
91
|
+
|
|
92
|
+
n: int
|
|
93
|
+
"""Current split number (starting at 1)."""
|
|
94
|
+
n_splits: int
|
|
95
|
+
"""Total split count."""
|
|
96
|
+
start: str
|
|
97
|
+
"""An :meth:`ISO-formatted <pandas.Timestamp.isoformat>` timestamp (see :attr:`.DatetimeSplitBounds.start`)."""
|
|
98
|
+
mid: str
|
|
99
|
+
"""An :meth:`ISO-formatted <pandas.Timestamp.isoformat>` timestamp (see :attr:`.DatetimeSplitBounds.mid`)."""
|
|
100
|
+
end: str
|
|
101
|
+
"""An :meth:`ISO-formatted <pandas.Timestamp.isoformat>` timestamp (see :attr:`.DatetimeSplitBounds.end`)."""
|
|
102
|
+
seconds: _t.NotRequired[float]
|
|
103
|
+
"""User time for the fold. Available only for the :attr:`fold-end message <.settings.log_split_progress.END_MESSAGE>`."""
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from collections.abc import Callable, Iterable, MutableMapping, Sequence
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from time import perf_counter
|
|
5
|
-
from typing import Any, Union
|
|
6
|
-
|
|
7
|
-
from rics.misc import get_by_full_name
|
|
8
|
-
|
|
9
|
-
from ..settings import log_split_progress as settings
|
|
10
|
-
from ..types import DatetimeSplitBounds
|
|
11
|
-
from ._to_string import _PrettyTimestamp
|
|
12
|
-
|
|
13
|
-
LoggerArg = Union[logging.Logger, logging.LoggerAdapter, str] # type: ignore[type-arg]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def log_split_progress(
|
|
17
|
-
splits: Sequence[DatetimeSplitBounds],
|
|
18
|
-
*,
|
|
19
|
-
logger: LoggerArg = "time_split",
|
|
20
|
-
start_level: int = logging.INFO,
|
|
21
|
-
end_level: int = logging.INFO,
|
|
22
|
-
extra: dict[str, Any] | None = None,
|
|
23
|
-
) -> Iterable[DatetimeSplitBounds]:
|
|
24
|
-
"""Log iteration progress over `splits` using `logger`.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
splits: Splits to iterate over.
|
|
28
|
-
logger: Logger or logger name to use.
|
|
29
|
-
start_level: Log level to use for the :attr:`fold-begin message <.settings.log_split_progress.START_MESSAGE>`.
|
|
30
|
-
end_level: Log level to use for the :attr:`fold-end message <.settings.log_split_progress.END_MESSAGE>`.
|
|
31
|
-
extra: User-defined `extra`-arguments to use when logging, merged with progress-related extras. Will be
|
|
32
|
-
available to all messages as well as the ``fold`` key. This argument is mutable; changes made to `extra`
|
|
33
|
-
will be reflected in logged records.
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
An iterable over `splits`.
|
|
37
|
-
|
|
38
|
-
Examples:
|
|
39
|
-
Basic usage.
|
|
40
|
-
|
|
41
|
-
>>> from time_split import split, log_split_progress
|
|
42
|
-
>>> splits = split("36h", available=("2023-08-10", "2023-08-19"))
|
|
43
|
-
>>> tracked_splits = log_split_progress(
|
|
44
|
-
... splits, logger="progress", start_level=logging.DEBUG
|
|
45
|
-
... )
|
|
46
|
-
>>> list(tracked_splits) # doctest: +SKIP
|
|
47
|
-
[progress:DEBUG] Begin fold 1/2: ('2023-08-11' <= [schedule: '2023-08-16' (Wednesday)] < '2023-08-17 12:00:00').
|
|
48
|
-
[progress:INFO] Finished fold 1/2 [schedule: '2023-08-16' (Wednesday)] after 5m 18s.
|
|
49
|
-
[progress:DEBUG] Begin fold 2/2: ('2023-08-12 12:00:00' <= [schedule: '2023-08-17 12:00:00' (Thursday)] < '2023-08-19').
|
|
50
|
-
[progress:INFO] Finished fold 2/2 [schedule: '2023-08-17 12:00:00' (Thursday)] after 4m 3s.
|
|
51
|
-
"""
|
|
52
|
-
logger = logging.getLogger(logger) if isinstance(logger, str) else logger
|
|
53
|
-
|
|
54
|
-
if isinstance(logger, logging.LoggerAdapter) and not hasattr(logger, "merge_extra"):
|
|
55
|
-
# Backport of https://github.com/python/cpython/pull/107292
|
|
56
|
-
logger = _MergingLoggerAdapter(logger.logger, logger.extra)
|
|
57
|
-
|
|
58
|
-
track = _ProgressTracker(
|
|
59
|
-
logger=logger,
|
|
60
|
-
fold_format=settings.FOLD_FORMAT,
|
|
61
|
-
start_level=start_level,
|
|
62
|
-
start_message=settings.START_MESSAGE,
|
|
63
|
-
end_level=end_level,
|
|
64
|
-
end_message=settings.END_MESSAGE,
|
|
65
|
-
seconds_formatter=settings.SECONDS_FORMATTER
|
|
66
|
-
if callable(settings.SECONDS_FORMATTER)
|
|
67
|
-
else get_by_full_name(settings.SECONDS_FORMATTER),
|
|
68
|
-
user_extra=extra or {},
|
|
69
|
-
)
|
|
70
|
-
return track(splits)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@dataclass(frozen=True)
|
|
74
|
-
class _ProgressTracker:
|
|
75
|
-
logger: logging.Logger | logging.LoggerAdapter # type: ignore[type-arg]
|
|
76
|
-
fold_format: str
|
|
77
|
-
start_level: int
|
|
78
|
-
start_message: str
|
|
79
|
-
end_level: int
|
|
80
|
-
end_message: str
|
|
81
|
-
seconds_formatter: Callable[[float], str]
|
|
82
|
-
user_extra: dict[str, Any]
|
|
83
|
-
|
|
84
|
-
def __call__(self, splits: Sequence[DatetimeSplitBounds]) -> Iterable[DatetimeSplitBounds]:
|
|
85
|
-
n_splits = len(splits)
|
|
86
|
-
|
|
87
|
-
for n, split in enumerate(splits, start=1):
|
|
88
|
-
extra: dict[str, str | float | int | bool] = dict(
|
|
89
|
-
n=n,
|
|
90
|
-
n_splits=n_splits,
|
|
91
|
-
start=split.start.isoformat(),
|
|
92
|
-
mid=split.mid.isoformat(),
|
|
93
|
-
end=split.end.isoformat(),
|
|
94
|
-
**self.user_extra,
|
|
95
|
-
)
|
|
96
|
-
kwargs: dict[str, Any] = dict(
|
|
97
|
-
n=n,
|
|
98
|
-
n_splits=n_splits,
|
|
99
|
-
start=_PrettyTimestamp(split.start),
|
|
100
|
-
mid=_PrettyTimestamp(split.mid),
|
|
101
|
-
end=_PrettyTimestamp(split.end),
|
|
102
|
-
**self.user_extra,
|
|
103
|
-
)
|
|
104
|
-
kwargs.update(fold=self.fold_format.format(**kwargs))
|
|
105
|
-
|
|
106
|
-
self.logger.log(self.start_level, self.start_message.format(**kwargs), extra=extra)
|
|
107
|
-
|
|
108
|
-
# Yield split and count user time.
|
|
109
|
-
start = perf_counter()
|
|
110
|
-
yield split
|
|
111
|
-
seconds = round(perf_counter() - start, 6)
|
|
112
|
-
|
|
113
|
-
kwargs.update(
|
|
114
|
-
seconds=seconds,
|
|
115
|
-
formatted_seconds=self.seconds_formatter(seconds),
|
|
116
|
-
)
|
|
117
|
-
extra.update(seconds=seconds)
|
|
118
|
-
self.logger.log(self.end_level, self.end_message.format(**kwargs), extra=extra)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
class _MergingLoggerAdapter(logging.LoggerAdapter): # type: ignore[type-arg]
|
|
122
|
-
def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> tuple[Any, MutableMapping[str, Any]]:
|
|
123
|
-
"""See https://github.com/python/cpython/pull/107292."""
|
|
124
|
-
kwargs["extra"] = {**self.extra, **kwargs["extra"]} if "extra" in kwargs and self.extra else self.extra
|
|
125
|
-
return msg, kwargs
|
|
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
|