time-split 0.4.0__tar.gz → 0.6.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.4.0 → time_split-0.6.0}/PKG-INFO +19 -2
- {time_split-0.4.0 → time_split-0.6.0}/README.md +17 -0
- {time_split-0.4.0 → time_split-0.6.0}/pyproject.toml +9 -8
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/__init__.py +1 -1
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_splitter.py +24 -28
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_docstrings.py +3 -1
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/__init__.py +2 -1
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/_plot.py +27 -7
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/_split.py +3 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/_to_string.py +50 -1
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/sklearn/_impl.py +5 -24
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/settings.py +9 -4
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/support/__init__.py +4 -1
- time_split-0.6.0/src/time_split/support/_create_explorer_link.py +118 -0
- {time_split-0.4.0 → time_split-0.6.0}/LICENSE.md +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/__init__.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_datetime_index_like.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_limits.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_process_available.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_schedule.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_span.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_compat.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/_progress.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/_weight.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_support.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/__init__.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/_log_progress.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/base.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/pandas/__init__.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/pandas/_impl.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/polars/__init__.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/polars/_impl.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/sklearn/__init__.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/py.typed +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/support/types.py +0 -0
- {time_split-0.4.0 → time_split-0.6.0}/src/time_split/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: time-split
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Time-based k-fold validation splits for heterogeneous data.
|
|
5
5
|
Home-page: https://github.com/rsundqvist/time-split
|
|
6
6
|
Author: Richard Sundqvist
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
21
21
|
Provides-Extra: plotting
|
|
22
22
|
Requires-Dist: matplotlib ; extra == "plotting"
|
|
23
23
|
Requires-Dist: pandas (>=2.0.3)
|
|
24
|
-
Requires-Dist: rics (>=
|
|
24
|
+
Requires-Dist: rics (>=4.1.1)
|
|
25
25
|
Requires-Dist: seaborn ; extra == "plotting"
|
|
26
26
|
Project-URL: Bug Tracker, https://github.com/rsundqvist/time-split/issues
|
|
27
27
|
Project-URL: Changelog, https://github.com/rsundqvist/time-split/blob/master/CHANGELOG.md
|
|
@@ -39,6 +39,7 @@ Time-based k-fold validation splits for heterogeneous data.
|
|
|
39
39
|
[](https://codecov.io/gh/rsundqvist/time-split)
|
|
40
40
|
[](https://time-split.readthedocs.io/)
|
|
41
41
|
[](https://pypi.python.org/pypi/time-split)
|
|
42
|
+
[](https://hub.docker.com/r/rsundqvist/time-split/)
|
|
42
43
|
|
|
43
44
|
<div align="center">
|
|
44
45
|
<img alt="Plotted folds on a two-by-two grid."
|
|
@@ -65,6 +66,22 @@ strong non-stationary characteristics.
|
|
|
65
66
|
[use custom text](https://time-split.readthedocs.io/en/stable/auto_examples/plotting_with_metrics.html).
|
|
66
67
|
- [Integrations](https://time-split.readthedocs.io/en/stable/generated/time_split.integration.html) for popular
|
|
67
68
|
libraries such as `pandas`, `polars` and `scikit-learn`.
|
|
69
|
+
- Convenient [web application](#experimenting-with-parameters) for exploring folds with different parameters.
|
|
70
|
+
|
|
71
|
+
## Experimenting with parameters
|
|
72
|
+
The **Time Fold Explorer** application
|
|
73
|
+
(available [here](https://time-split.streamlit.app/?data=1554942900-1557610200&schedule=0+0+%2A+%2A+MON%2CFRI&n_splits=2&step=2&show_removed=True))
|
|
74
|
+
is designed to help evaluate the effects of different
|
|
75
|
+
[parameters](https://time-split.readthedocs.io/en/stable/#parameter-overview).
|
|
76
|
+
To start it locally using
|
|
77
|
+
[](https://hub.docker.com/r/rsundqvist/time-split/)
|
|
78
|
+
Docker, run
|
|
79
|
+
```sh
|
|
80
|
+
docker run -p 8501:8501 rsundqvist/time-split
|
|
81
|
+
```
|
|
82
|
+
in the terminal. You may use
|
|
83
|
+
[`create_explorer_link()`](https://time-split.readthedocs.io/en/stable/generated/time_split.support.html#time_split.support.create_explorer_link)
|
|
84
|
+
to build application URLs with preselected splitting parameters.
|
|
68
85
|
|
|
69
86
|
## Installation
|
|
70
87
|
The package is published through the [Python Package Index (PyPI)]. Source code
|
|
@@ -8,6 +8,7 @@ Time-based k-fold validation splits for heterogeneous data.
|
|
|
8
8
|
[](https://codecov.io/gh/rsundqvist/time-split)
|
|
9
9
|
[](https://time-split.readthedocs.io/)
|
|
10
10
|
[](https://pypi.python.org/pypi/time-split)
|
|
11
|
+
[](https://hub.docker.com/r/rsundqvist/time-split/)
|
|
11
12
|
|
|
12
13
|
<div align="center">
|
|
13
14
|
<img alt="Plotted folds on a two-by-two grid."
|
|
@@ -34,6 +35,22 @@ strong non-stationary characteristics.
|
|
|
34
35
|
[use custom text](https://time-split.readthedocs.io/en/stable/auto_examples/plotting_with_metrics.html).
|
|
35
36
|
- [Integrations](https://time-split.readthedocs.io/en/stable/generated/time_split.integration.html) for popular
|
|
36
37
|
libraries such as `pandas`, `polars` and `scikit-learn`.
|
|
38
|
+
- Convenient [web application](#experimenting-with-parameters) for exploring folds with different parameters.
|
|
39
|
+
|
|
40
|
+
## Experimenting with parameters
|
|
41
|
+
The **Time Fold Explorer** application
|
|
42
|
+
(available [here](https://time-split.streamlit.app/?data=1554942900-1557610200&schedule=0+0+%2A+%2A+MON%2CFRI&n_splits=2&step=2&show_removed=True))
|
|
43
|
+
is designed to help evaluate the effects of different
|
|
44
|
+
[parameters](https://time-split.readthedocs.io/en/stable/#parameter-overview).
|
|
45
|
+
To start it locally using
|
|
46
|
+
[](https://hub.docker.com/r/rsundqvist/time-split/)
|
|
47
|
+
Docker, run
|
|
48
|
+
```sh
|
|
49
|
+
docker run -p 8501:8501 rsundqvist/time-split
|
|
50
|
+
```
|
|
51
|
+
in the terminal. You may use
|
|
52
|
+
[`create_explorer_link()`](https://time-split.readthedocs.io/en/stable/generated/time_split.support.html#time_split.support.create_explorer_link)
|
|
53
|
+
to build application URLs with preselected splitting parameters.
|
|
37
54
|
|
|
38
55
|
## Installation
|
|
39
56
|
The package is published through the [Python Package Index (PyPI)]. Source code
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "time-split"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Time-based k-fold validation splits for heterogeneous data."
|
|
5
5
|
authors = ["Richard Sundqvist <richard.sundqvist@live.se>"]
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
|
18
18
|
"License :: OSI Approved :: MIT License",
|
|
19
19
|
"Natural Language :: English",
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
21
|
-
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
22
|
"Programming Language :: Python :: 3.11",
|
|
23
23
|
"Programming Language :: Python :: 3.12",
|
|
24
24
|
]
|
|
@@ -34,7 +34,7 @@ classifiers = [
|
|
|
34
34
|
python = ">=3.11"
|
|
35
35
|
|
|
36
36
|
pandas = ">=2.0.3"
|
|
37
|
-
rics = ">=
|
|
37
|
+
rics = ">=4.1.1"
|
|
38
38
|
|
|
39
39
|
# plotting
|
|
40
40
|
matplotlib = { version = "*", optional = true }
|
|
@@ -48,7 +48,7 @@ plotting = ["matplotlib", "seaborn"]
|
|
|
48
48
|
croniter = "^2.0.3"
|
|
49
49
|
pyarrow = ">=15.0.2,<17.0.0"
|
|
50
50
|
polars = { version = "^0.20.19", extras = ["pandas"] }
|
|
51
|
-
|
|
51
|
+
streamlit = "*"
|
|
52
52
|
|
|
53
53
|
[tool.poetry.group.docs]
|
|
54
54
|
[tool.poetry.group.docs.dependencies]
|
|
@@ -85,11 +85,11 @@ dask = { version = "==2024.5.0", extras = ["dataframe"] }
|
|
|
85
85
|
|
|
86
86
|
[tool.poetry.group.devops.dependencies]
|
|
87
87
|
invoke = "^2.2.0"
|
|
88
|
-
mypy = "^1.
|
|
89
|
-
ruff = "
|
|
88
|
+
mypy = "^1.10.1"
|
|
89
|
+
ruff = "^0.5.0"
|
|
90
90
|
|
|
91
|
-
safety = "^3.
|
|
92
|
-
codespell = "^2.
|
|
91
|
+
safety = "^3.2.3"
|
|
92
|
+
codespell = "^2.3.0"
|
|
93
93
|
bump2version = "^1.0.1"
|
|
94
94
|
|
|
95
95
|
[tool.ruff]
|
|
@@ -148,6 +148,7 @@ ignore = [
|
|
|
148
148
|
"SIM108", # Use ternary operator - coverage issue, see https://github.com/nedbat/coveragepy/issues/509
|
|
149
149
|
"TD003", # Missing issue link
|
|
150
150
|
]
|
|
151
|
+
allowed-confusables = ["ℹ"]
|
|
151
152
|
|
|
152
153
|
[tool.ruff.lint.per-file-ignores]
|
|
153
154
|
"tests/*" = [
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from dataclasses import asdict, dataclass
|
|
3
|
+
from functools import lru_cache
|
|
3
4
|
from typing import cast, get_args
|
|
4
5
|
|
|
5
|
-
from pandas import Timedelta
|
|
6
|
+
from pandas import Timedelta
|
|
6
7
|
from rics.misc import format_kwargs
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
from ..settings import misc
|
|
9
|
+
from ..settings import misc as settings
|
|
11
10
|
from ..types import (
|
|
12
11
|
DatetimeIndexSplitterKwargs,
|
|
13
12
|
DatetimeIterable,
|
|
@@ -30,11 +29,12 @@ class DatetimeIndexSplitter:
|
|
|
30
29
|
"""Backend interface for splitting user data. See the :ref:`Parameter overview` page."""
|
|
31
30
|
|
|
32
31
|
schedule: Schedule
|
|
33
|
-
before: Span
|
|
34
|
-
after: Span
|
|
35
|
-
step: int
|
|
36
|
-
n_splits: int
|
|
37
|
-
expand_limits: ExpandLimits
|
|
32
|
+
before: Span = "7d"
|
|
33
|
+
after: Span = 1
|
|
34
|
+
step: int = 1
|
|
35
|
+
n_splits: int = 0
|
|
36
|
+
expand_limits: ExpandLimits = "auto"
|
|
37
|
+
ignore_filters: bool = False
|
|
38
38
|
|
|
39
39
|
def get_splits(self, available: DatetimeIterable | None = None) -> DatetimeSplits:
|
|
40
40
|
"""Compute a split of given user data."""
|
|
@@ -55,7 +55,7 @@ class DatetimeIndexSplitter:
|
|
|
55
55
|
|
|
56
56
|
types = get_args(TimedeltaTypes)
|
|
57
57
|
if (
|
|
58
|
-
|
|
58
|
+
settings.snap_to_end
|
|
59
59
|
and self.after != "all"
|
|
60
60
|
and isinstance(self.schedule, types)
|
|
61
61
|
and isinstance(self.after, types)
|
|
@@ -99,7 +99,7 @@ class DatetimeIndexSplitter:
|
|
|
99
99
|
msg = f"No valid splits with {limits_info}split params: ({format_kwargs(self.as_dict())})"
|
|
100
100
|
raise ValueError(msg)
|
|
101
101
|
|
|
102
|
-
return self._filter(retval)
|
|
102
|
+
return retval if self.ignore_filters else self._filter(retval)
|
|
103
103
|
|
|
104
104
|
def _filter(self, splits: DatetimeSplits) -> DatetimeSplits:
|
|
105
105
|
"""Apply splitting arguments.
|
|
@@ -122,29 +122,25 @@ class DatetimeIndexSplitter:
|
|
|
122
122
|
if self.step < 0: # Poorly documented - might not work as expected?
|
|
123
123
|
splits.reverse()
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
if settings.filter is None:
|
|
126
|
+
return splits
|
|
127
|
+
|
|
128
|
+
return [s for s in splits if settings.filter(*s)]
|
|
126
129
|
|
|
127
130
|
def _log_expansion(self, original: LimitsTuple, *, expanded: LimitsTuple) -> None:
|
|
128
|
-
|
|
129
|
-
return
|
|
131
|
+
from .._frontend import format_expanded_limits
|
|
130
132
|
|
|
131
|
-
if not LOGGER.isEnabledFor(logging.INFO):
|
|
133
|
+
if original == expanded or not LOGGER.isEnabledFor(logging.INFO):
|
|
132
134
|
return
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
message = format_expanded_limits(original, expanded=expanded, expand_limits=self.expand_limits)
|
|
137
|
+
self._emit_once(message)
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
LOGGER.info(
|
|
144
|
-
f"Available data limits have been expanded (since expand_limits={self.expand_limits!r}):\n"
|
|
145
|
-
f" start: {stringify(original[0], new=expanded[0])}\n"
|
|
146
|
-
f" end: {stringify(original[1], new=expanded[1])}"
|
|
147
|
-
)
|
|
139
|
+
@staticmethod
|
|
140
|
+
@lru_cache(8)
|
|
141
|
+
def _emit_once(message: str) -> None:
|
|
142
|
+
"""Emit if **not** in the eight most recent emission messages."""
|
|
143
|
+
LOGGER.info(message)
|
|
148
144
|
|
|
149
145
|
def __post_init__(self) -> None:
|
|
150
146
|
# Verify n_splits
|
|
@@ -17,7 +17,7 @@ def docs(__func: Callable[P, T], /) -> Callable[P, T]:
|
|
|
17
17
|
__func.__doc__ = __func.__doc__.format(**_DOCSTRINGS)
|
|
18
18
|
|
|
19
19
|
if isinstance(__func, type):
|
|
20
|
-
__func.__init__ = deprecated_params(__func.__init__)
|
|
20
|
+
__func.__init__ = deprecated_params(__func.__init__) # type: ignore[misc]
|
|
21
21
|
else:
|
|
22
22
|
__func = deprecated_params(__func)
|
|
23
23
|
|
|
@@ -37,10 +37,12 @@ _DOCSTRINGS = {
|
|
|
37
37
|
"n_splits": "Maximum number of folds, preferring folds later in the schedule.",
|
|
38
38
|
"available": "Binds `schedule` to a range.",
|
|
39
39
|
"expand_limits": f'A {_OFFSET} used to expand `available` data to its likely `"true"` limits. Pass ``False`` to disable.',
|
|
40
|
+
"ignore_filters": "Set to ignore filtering parameters (e.g. `step`) and global configuration.",
|
|
40
41
|
"USER_GUIDE": (
|
|
41
42
|
"For more information about the `schedule`, `before/after` and `expand_limits`-arguments"
|
|
42
43
|
", see the :ref:`User guide`."
|
|
43
44
|
),
|
|
44
45
|
"OFFSET": _OFFSET,
|
|
45
46
|
"DatetimeIndexSplitterKwargs": "See :func:`~time_split.split`. The `available` keyword is managed by the integration.",
|
|
47
|
+
"show_removed": "If ``True``, splits removed by `n_splits` or `step` are included in the figure.",
|
|
46
48
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from ._plot import plot
|
|
2
2
|
from ._progress import default_metrics_formatter, log_split_progress
|
|
3
3
|
from ._split import split
|
|
4
|
-
from ._to_string import to_string
|
|
4
|
+
from ._to_string import format_expanded_limits, to_string
|
|
5
5
|
from ._weight import fold_weight
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
@@ -11,4 +11,5 @@ __all__ = [
|
|
|
11
11
|
"plot",
|
|
12
12
|
"split",
|
|
13
13
|
"to_string",
|
|
14
|
+
"format_expanded_limits",
|
|
14
15
|
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from dataclasses import asdict, dataclass, replace
|
|
2
|
-
from typing import TYPE_CHECKING, Any, Literal
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Literal, Sized
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
from pandas import Timestamp
|
|
@@ -22,6 +22,7 @@ from ..types import (
|
|
|
22
22
|
Span,
|
|
23
23
|
)
|
|
24
24
|
from ._split import split
|
|
25
|
+
from ._to_string import _PrettyTimestamp
|
|
25
26
|
from ._weight import fold_weight
|
|
26
27
|
|
|
27
28
|
if TYPE_CHECKING:
|
|
@@ -66,6 +67,7 @@ def plot(
|
|
|
66
67
|
n_splits: int = 0,
|
|
67
68
|
available: DatetimeIterable | None = None,
|
|
68
69
|
expand_limits: ExpandLimits = "auto",
|
|
70
|
+
ignore_filters: bool = False,
|
|
69
71
|
# Split plot args
|
|
70
72
|
bar_labels: str | Rows | list[tuple[str, str]] | bool = True,
|
|
71
73
|
show_removed: bool = False,
|
|
@@ -86,12 +88,13 @@ def plot(
|
|
|
86
88
|
available: {available} If `bar_labels` is given but is not a ``list``,
|
|
87
89
|
this data will be used to compute fold sizes.
|
|
88
90
|
expand_limits: {expand_limits}
|
|
91
|
+
ignore_filters: {ignore_filters}
|
|
89
92
|
bar_labels: Labels to draw on the bars. If you pass a string, it will be interpreted as a time unit (see
|
|
90
93
|
:ref:`pandas:timeseries.offset_aliases` for valid frequency strings). Bars will show the number of units
|
|
91
94
|
contained. Pass `'rows'` to simply count the numbers of elements in `data` (if given). To write custom
|
|
92
95
|
bar labels, pass a list ``[(data_label, future_data_label), ...]``, one tuple for each fold. This may be
|
|
93
96
|
used to write metric values per data set after cross validation.
|
|
94
|
-
show_removed:
|
|
97
|
+
show_removed: {show_removed}
|
|
95
98
|
row_count_bin: A {OFFSET}. If given, show normalized row count per `row_count_bin` in the background. Pass
|
|
96
99
|
``pandas.Series`` to use pre-computed row counts.
|
|
97
100
|
ax: Axis to use for plotting. If ``None``, create new axes.
|
|
@@ -116,20 +119,24 @@ def plot(
|
|
|
116
119
|
step=step,
|
|
117
120
|
n_splits=n_splits,
|
|
118
121
|
expand_limits=expand_limits,
|
|
122
|
+
ignore_filters=ignore_filters,
|
|
119
123
|
)
|
|
120
124
|
plot_data = _get_plot_data(available, splitter, row_count_bin=row_count_bin, show_removed=show_removed)
|
|
121
125
|
|
|
122
126
|
if bar_labels is True:
|
|
123
|
-
bar_labels =
|
|
127
|
+
bar_labels = (
|
|
128
|
+
settings.DEFAULT_TIME_UNIT if (plot_data.available is None or _is_limits(available)) else COUNT_ROWS
|
|
129
|
+
)
|
|
124
130
|
|
|
125
131
|
if ax is None:
|
|
126
|
-
|
|
132
|
+
fig, ax = plt.subplots(
|
|
127
133
|
tight_layout=True,
|
|
128
134
|
figsize=(
|
|
129
135
|
plt.rcParams["figure.figsize"][0],
|
|
130
136
|
3 + len(plot_data.splits) * 0.5,
|
|
131
137
|
),
|
|
132
138
|
)
|
|
139
|
+
fig.autofmt_xdate(ha="center", rotation=15)
|
|
133
140
|
|
|
134
141
|
_plot_splits(ax, plot_data.splits, removed=plot_data.removed)
|
|
135
142
|
|
|
@@ -166,7 +173,7 @@ def _plot_limits(ax: "Axes", limits: LimitsTuple) -> None:
|
|
|
166
173
|
from matplotlib.dates import date2num
|
|
167
174
|
|
|
168
175
|
left, right = limits
|
|
169
|
-
ax.axvline(left, color="k", ls="--", label="
|
|
176
|
+
ax.axvline(left, color="k", ls="--", label="Available")
|
|
170
177
|
ax.axvline(right, color="k", ls="--")
|
|
171
178
|
left_tick, right_tick = date2num(left), date2num(right) # type: ignore[no-untyped-call]
|
|
172
179
|
ax.set_xticks([left_tick, *ax.get_xticks(), right_tick])
|
|
@@ -284,7 +291,8 @@ def _get_plot_data(
|
|
|
284
291
|
|
|
285
292
|
if show_removed:
|
|
286
293
|
kept_splits = set(splits)
|
|
287
|
-
splits = replace(splitter,
|
|
294
|
+
splits = replace(splitter, ignore_filters=True).get_plot_data(available)[0]
|
|
295
|
+
|
|
288
296
|
if splitter.step < 0:
|
|
289
297
|
splits.reverse()
|
|
290
298
|
removed = set(splits) - set(kept_splits)
|
|
@@ -347,7 +355,19 @@ def _make_title(available: Any | None, split_kwargs: dict[str, Any]) -> str:
|
|
|
347
355
|
kwargs = {key: value for key, value in split_kwargs.items() if not is_default(key)}
|
|
348
356
|
if available is None:
|
|
349
357
|
formatted_available = ""
|
|
358
|
+
elif _is_limits(available):
|
|
359
|
+
available = sorted(_PrettyTimestamp(a).auto for a in available)
|
|
360
|
+
available = tuple(available)
|
|
361
|
+
formatted_available = f", {available=}" # Probably pre-computed
|
|
350
362
|
else:
|
|
351
363
|
pretty = get_public_module(type(available), resolve_reexport=True, include_name=True)
|
|
352
364
|
formatted_available = f", available={pretty}"
|
|
353
|
-
|
|
365
|
+
|
|
366
|
+
title = f"time_split.split({format_kwargs(kwargs, max_value_length=40)}{formatted_available})"
|
|
367
|
+
title = title.replace("datetime.", "")
|
|
368
|
+
title = title.replace("pandas.", "pd.")
|
|
369
|
+
return title
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _is_limits(available: Any) -> bool:
|
|
373
|
+
return isinstance(available, Sized) and len(available) == 2 # noqa: PLR2004
|
|
@@ -13,6 +13,7 @@ def split(
|
|
|
13
13
|
n_splits: int = 0,
|
|
14
14
|
available: DatetimeIterable | None = None,
|
|
15
15
|
expand_limits: ExpandLimits = "auto",
|
|
16
|
+
ignore_filters: bool = False,
|
|
16
17
|
) -> DatetimeSplits:
|
|
17
18
|
"""Create time-based cross-validation splits.
|
|
18
19
|
|
|
@@ -26,6 +27,7 @@ def split(
|
|
|
26
27
|
n_splits: {n_splits}
|
|
27
28
|
available: {available} Passing a tuple ``(min, max)`` is enough.
|
|
28
29
|
expand_limits: {expand_limits}
|
|
30
|
+
ignore_filters: {ignore_filters}
|
|
29
31
|
|
|
30
32
|
{USER_GUIDE}
|
|
31
33
|
|
|
@@ -42,4 +44,5 @@ def split(
|
|
|
42
44
|
step=step,
|
|
43
45
|
n_splits=n_splits,
|
|
44
46
|
expand_limits=expand_limits,
|
|
47
|
+
ignore_filters=ignore_filters,
|
|
45
48
|
).get_splits(available)
|
|
@@ -2,8 +2,10 @@ from typing import Any, Literal, overload
|
|
|
2
2
|
|
|
3
3
|
from pandas import Timestamp
|
|
4
4
|
|
|
5
|
+
from .._backend._limits import LimitsTuple
|
|
6
|
+
from .._compat import fmt_sec
|
|
5
7
|
from ..settings import log_split_progress
|
|
6
|
-
from ..types import DatetimeSplitBounds, DatetimeTypes
|
|
8
|
+
from ..types import DatetimeSplitBounds, DatetimeTypes, ExpandLimits
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
@overload
|
|
@@ -73,6 +75,25 @@ def to_string(
|
|
|
73
75
|
)
|
|
74
76
|
|
|
75
77
|
|
|
78
|
+
def stringify(old: Timestamp, *, new: Timestamp | None = None, diff_only: bool = False) -> str:
|
|
79
|
+
if new is None:
|
|
80
|
+
return _PrettyTimestamp(old).auto
|
|
81
|
+
|
|
82
|
+
original = f"{old} -> "
|
|
83
|
+
if old == new:
|
|
84
|
+
no_change = "<no change>"
|
|
85
|
+
if diff_only:
|
|
86
|
+
return no_change
|
|
87
|
+
return original + no_change
|
|
88
|
+
diff = (new - old).total_seconds()
|
|
89
|
+
pretty_diff = f"{'+' if diff > 0 else '-'}{fmt_sec(abs(diff))}"
|
|
90
|
+
|
|
91
|
+
if diff_only:
|
|
92
|
+
return pretty_diff
|
|
93
|
+
|
|
94
|
+
return original + f"{_PrettyTimestamp(new).auto} ({pretty_diff})"
|
|
95
|
+
|
|
96
|
+
|
|
76
97
|
class _PrettyTimestamp:
|
|
77
98
|
def __init__(self, raw: DatetimeTypes) -> None:
|
|
78
99
|
self.timestamp = Timestamp(raw)
|
|
@@ -101,3 +122,31 @@ class _PrettyTimestamp:
|
|
|
101
122
|
)
|
|
102
123
|
self._auto = format(timestamp, format_spec)
|
|
103
124
|
return self._auto
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def format_expanded_limits(
|
|
128
|
+
original: LimitsTuple,
|
|
129
|
+
*,
|
|
130
|
+
expanded: LimitsTuple | None = None,
|
|
131
|
+
expand_limits: ExpandLimits,
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Format expanded limits.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
original: The original data limits.
|
|
137
|
+
expanded: Expanded data limits. Derived based on `original` and `expanded_limits` if ``None``.
|
|
138
|
+
expand_limits: Limits expansion spec.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
A string.
|
|
142
|
+
"""
|
|
143
|
+
if expanded is None:
|
|
144
|
+
from .._backend._limits import expand_limits as expand
|
|
145
|
+
|
|
146
|
+
expanded = expand(original, spec=expand_limits)
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
f"Available data limits have been expanded (since {expand_limits=}):\n"
|
|
150
|
+
f" start: {stringify(original[0], new=expanded[0])}\n"
|
|
151
|
+
f" end: {stringify(original[1], new=expanded[1])}"
|
|
152
|
+
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections.abc import Iterable, Sequence
|
|
2
|
-
from typing import Any, cast, get_args
|
|
2
|
+
from typing import Any, Unpack, cast, get_args
|
|
3
3
|
|
|
4
4
|
from numpy import array, datetime64, logical_and, ndarray, nonzero
|
|
5
5
|
from numpy.typing import NDArray
|
|
@@ -7,13 +7,11 @@ from numpy.typing import NDArray
|
|
|
7
7
|
from ..._backend import DatetimeIndexSplitter
|
|
8
8
|
from ..._docstrings import docs
|
|
9
9
|
from ...types import (
|
|
10
|
+
DatetimeIndexSplitterKwargs,
|
|
10
11
|
DatetimeIterable,
|
|
11
12
|
DatetimeSplits,
|
|
12
13
|
DatetimeTypes,
|
|
13
|
-
ExpandLimits,
|
|
14
14
|
MetricsType,
|
|
15
|
-
Schedule,
|
|
16
|
-
Span,
|
|
17
15
|
)
|
|
18
16
|
from .._log_progress import LogProgressArg, handle_log_progress_arg
|
|
19
17
|
|
|
@@ -39,14 +37,9 @@ class ScikitLearnSplitter(BaseCrossValidator): # type: ignore[misc]
|
|
|
39
37
|
If a ``pandas`` type is passed to the :meth:`ScikitLearnSplitter.split`-method, the index will be used.
|
|
40
38
|
|
|
41
39
|
Args:
|
|
42
|
-
schedule: {schedule}
|
|
43
|
-
before: {before}
|
|
44
|
-
after: {after}
|
|
45
|
-
step: {step}
|
|
46
|
-
n_splits: {n_splits}
|
|
47
|
-
expand_limits: {expand_limits}
|
|
48
40
|
log_progress: {log_progress}
|
|
49
41
|
verify_xy: If ``True``, split X and y independently and verify that they are equal.
|
|
42
|
+
**kwargs: {DatetimeIndexSplitterKwargs}
|
|
50
43
|
|
|
51
44
|
{USER_GUIDE}
|
|
52
45
|
|
|
@@ -54,25 +47,13 @@ class ScikitLearnSplitter(BaseCrossValidator): # type: ignore[misc]
|
|
|
54
47
|
|
|
55
48
|
def __init__(
|
|
56
49
|
self,
|
|
57
|
-
schedule: Schedule,
|
|
58
50
|
*,
|
|
59
|
-
before: Span = "7d",
|
|
60
|
-
after: Span = 1,
|
|
61
|
-
n_splits: int = 0,
|
|
62
|
-
expand_limits: ExpandLimits = "auto",
|
|
63
|
-
step: int = 1,
|
|
64
51
|
log_progress: LogProgressArg[MetricsType] = False,
|
|
65
52
|
verify_xy: bool = True,
|
|
53
|
+
**kwargs: Unpack[DatetimeIndexSplitterKwargs],
|
|
66
54
|
) -> None:
|
|
67
55
|
super().__init__()
|
|
68
|
-
self._splitter = DatetimeIndexSplitter(
|
|
69
|
-
schedule,
|
|
70
|
-
before=before,
|
|
71
|
-
after=after,
|
|
72
|
-
step=step,
|
|
73
|
-
n_splits=n_splits,
|
|
74
|
-
expand_limits=expand_limits,
|
|
75
|
-
)
|
|
56
|
+
self._splitter = DatetimeIndexSplitter(**kwargs)
|
|
76
57
|
self.log_progress = log_progress
|
|
77
58
|
self.verify_xy = verify_xy
|
|
78
59
|
|
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
import typing as _t
|
|
4
4
|
|
|
5
|
+
import pandas as _pd
|
|
6
|
+
|
|
5
7
|
from . import types as _tst
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class auto_expand_limits: # noqa: N801
|
|
9
11
|
"""Configuration for the `'auto'` :attr:`~time_split.types.ExpandLimits` logic.
|
|
10
12
|
|
|
11
|
-
This class determines how ``(min, max)``-tuples are expanded when
|
|
13
|
+
This class determines how ``(min, max)``-tuples are expanded when expanded limits are enabled.
|
|
12
14
|
"""
|
|
13
15
|
|
|
14
16
|
SANITY_CHECK: bool = True
|
|
15
|
-
"""If ``True``, use original limits if
|
|
17
|
+
"""If ``True``, use original limits if expanded limits do not pass sanity checks."""
|
|
16
18
|
|
|
17
19
|
@classmethod
|
|
18
20
|
def set_level(
|
|
@@ -84,8 +86,8 @@ class plot: # noqa: N801
|
|
|
84
86
|
FUTURE_DATA_LABEL: str = "Future data"
|
|
85
87
|
"""Label of the red bar; data in the the [:attr:`~.DatetimeSplitBounds.mid`, :attr:`~.types.DatetimeSplitBounds.end`)-range."""
|
|
86
88
|
|
|
87
|
-
DEFAULT_TIME_UNIT: str = "
|
|
88
|
-
"""Time unit to use by default when ``bar_labels=True`` and ``available=None
|
|
89
|
+
DEFAULT_TIME_UNIT: str = "hours"
|
|
90
|
+
"""Time unit to use by default when ``bar_labels=True`` and ``available=None`` or a limits tuple."""
|
|
89
91
|
|
|
90
92
|
REMOVED_FOLD_STYLE: _t.ClassVar[dict[str, _t.Any]] = {
|
|
91
93
|
"alpha": 0.35,
|
|
@@ -172,3 +174,6 @@ class misc: # noqa: N801
|
|
|
172
174
|
If ``True``, ensure that the `Future data` of the final fold ends at the rightmost edge of the available data range.
|
|
173
175
|
Otherwise, alignment is determined by :func:`pandas.date_range`.
|
|
174
176
|
"""
|
|
177
|
+
|
|
178
|
+
filter: _t.Callable[[_pd.Timestamp, _pd.Timestamp, _pd.Timestamp], bool] | None = None
|
|
179
|
+
"""A callable ``(start, mid, end) -> bool`` applied to all generated folds."""
|
|
@@ -12,7 +12,8 @@ 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 default_metrics_formatter, fold_weight, to_string
|
|
15
|
+
from .._frontend import default_metrics_formatter, fold_weight, format_expanded_limits, to_string
|
|
16
|
+
from ._create_explorer_link import create_explorer_link
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
"DatetimeIndexSplitter",
|
|
@@ -21,4 +22,6 @@ __all__ = [
|
|
|
21
22
|
"default_metrics_formatter",
|
|
22
23
|
"fold_weight",
|
|
23
24
|
"to_string",
|
|
25
|
+
"format_expanded_limits",
|
|
26
|
+
"create_explorer_link",
|
|
24
27
|
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Unpack
|
|
3
|
+
from urllib.parse import ParseResult, urlencode, urlparse, urlunparse
|
|
4
|
+
|
|
5
|
+
from .._backend import DatetimeIndexSplitter
|
|
6
|
+
from .._docstrings import docs
|
|
7
|
+
from ..types import DatetimeIndexSplitterKwargs, DatetimeIterable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@docs
|
|
11
|
+
def create_explorer_link(
|
|
12
|
+
host: str,
|
|
13
|
+
data: DatetimeIterable | str | None = None,
|
|
14
|
+
available: DatetimeIterable | str | None = None,
|
|
15
|
+
*,
|
|
16
|
+
show_removed: bool = True,
|
|
17
|
+
skip_default: bool = False,
|
|
18
|
+
**kwargs: Unpack[DatetimeIndexSplitterKwargs],
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Create a `Time Fold Explorer` application URL.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
host: Base address where the application is hosted.
|
|
24
|
+
data: {available} Regular `available` arguments (as passed to e.g. :func:`time_split.split`) are encoded as
|
|
25
|
+
a date range to generate dummy data for. Pass a ``str`` to use dataset bundled by the server instead. Note
|
|
26
|
+
that this function cannot verify the `kwargs` if `available` is a ``str`` dataset.
|
|
27
|
+
available: Alias of `data`.
|
|
28
|
+
skip_default: If ``True``, do not include default split params in the link.
|
|
29
|
+
show_removed: {show_removed}
|
|
30
|
+
kwargs: Keyword arguments for the :func:`time_split.split`-function.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
An encoded URL.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
Getting the URL for a local host.
|
|
37
|
+
|
|
38
|
+
>>> create_explorer_link(
|
|
39
|
+
... host="http://localhost:8501",
|
|
40
|
+
... available=("2019-04-11 00:35:00", "2019-05-11 21:30:00"),
|
|
41
|
+
... schedule="0 0 * * MON,FRI",
|
|
42
|
+
... )
|
|
43
|
+
'http://localhost:8501?data=1554942900-1557610200&schedule=0+0+%2A+%2A+MON%2CFRI&show_removed=True'
|
|
44
|
+
|
|
45
|
+
To start the application using locally using |image| Docker, run
|
|
46
|
+
|
|
47
|
+
.. code-block:: bash
|
|
48
|
+
|
|
49
|
+
docker run -p 8501:8501 rsundqvist/time-split
|
|
50
|
+
|
|
51
|
+
in the terminal.
|
|
52
|
+
|
|
53
|
+
.. |image| image:: https://img.shields.io/docker/image-size/rsundqvist/time-split/latest?logo=docker&label=time-split
|
|
54
|
+
:target: https://hub.docker.com/r/rsundqvist/time-split/
|
|
55
|
+
:alt: Docker Image Size (tag)
|
|
56
|
+
"""
|
|
57
|
+
if sum([data is None, available is None]) != 1:
|
|
58
|
+
raise ValueError("Exactly one of `data` and `available` must be given.")
|
|
59
|
+
if data is None:
|
|
60
|
+
data = available
|
|
61
|
+
|
|
62
|
+
assert data is not None # noqa: S101
|
|
63
|
+
pr = _create_explorer_link(host, data, show_removed=show_removed, skip_default=skip_default, **kwargs)
|
|
64
|
+
return urlunparse(pr)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _create_explorer_link(
|
|
68
|
+
host: str,
|
|
69
|
+
data: DatetimeIterable | str,
|
|
70
|
+
*,
|
|
71
|
+
show_removed: bool = True,
|
|
72
|
+
skip_default: bool = False,
|
|
73
|
+
**kwargs: Unpack[DatetimeIndexSplitterKwargs],
|
|
74
|
+
) -> ParseResult:
|
|
75
|
+
query = _create_query(data, skip_default=skip_default, show_removed=show_removed, kwargs=kwargs)
|
|
76
|
+
|
|
77
|
+
pr: ParseResult = urlparse(host) # scheme netloc path params query fragment
|
|
78
|
+
if pr.query:
|
|
79
|
+
raise ValueError(f"Bad {host=}; query={pr.query!r} is not allowed.")
|
|
80
|
+
return pr._replace(query=urlencode(query))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _create_query(
|
|
84
|
+
data: DatetimeIterable | str,
|
|
85
|
+
*,
|
|
86
|
+
skip_default: bool,
|
|
87
|
+
show_removed: bool,
|
|
88
|
+
kwargs: DatetimeIndexSplitterKwargs,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
query: dict[str, Any] = {}
|
|
91
|
+
|
|
92
|
+
if isinstance(data, str):
|
|
93
|
+
query = {"data": data, **kwargs}
|
|
94
|
+
else:
|
|
95
|
+
splitter = DatetimeIndexSplitter(**kwargs)
|
|
96
|
+
_, ms = splitter.get_plot_data(data)
|
|
97
|
+
query["data"] = _get_data_param(*ms.available_metadata.limits)
|
|
98
|
+
|
|
99
|
+
if skip_default:
|
|
100
|
+
schedule = kwargs["schedule"]
|
|
101
|
+
query["schedule"] = schedule
|
|
102
|
+
|
|
103
|
+
default = DatetimeIndexSplitter(schedule).as_dict()
|
|
104
|
+
no_defaults = {
|
|
105
|
+
key: value
|
|
106
|
+
for key, value in kwargs.items()
|
|
107
|
+
if value != default[key] # type: ignore[literal-required]
|
|
108
|
+
}
|
|
109
|
+
query.update(no_defaults)
|
|
110
|
+
else:
|
|
111
|
+
query.update(kwargs)
|
|
112
|
+
|
|
113
|
+
query["show_removed"] = show_removed
|
|
114
|
+
return query
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _get_data_param(start: datetime, end: datetime) -> str:
|
|
118
|
+
return f"{int(start.timestamp())}-{int(end.timestamp())}"
|
|
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
|