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.
Files changed (36) hide show
  1. {time_split-0.4.0 → time_split-0.6.0}/PKG-INFO +19 -2
  2. {time_split-0.4.0 → time_split-0.6.0}/README.md +17 -0
  3. {time_split-0.4.0 → time_split-0.6.0}/pyproject.toml +9 -8
  4. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/__init__.py +1 -1
  5. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_splitter.py +24 -28
  6. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_docstrings.py +3 -1
  7. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/__init__.py +2 -1
  8. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/_plot.py +27 -7
  9. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/_split.py +3 -0
  10. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/_to_string.py +50 -1
  11. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/sklearn/_impl.py +5 -24
  12. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/settings.py +9 -4
  13. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/support/__init__.py +4 -1
  14. time_split-0.6.0/src/time_split/support/_create_explorer_link.py +118 -0
  15. {time_split-0.4.0 → time_split-0.6.0}/LICENSE.md +0 -0
  16. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/__init__.py +0 -0
  17. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_datetime_index_like.py +0 -0
  18. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_limits.py +0 -0
  19. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_process_available.py +0 -0
  20. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_schedule.py +0 -0
  21. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_backend/_span.py +0 -0
  22. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_compat.py +0 -0
  23. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/_progress.py +0 -0
  24. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_frontend/_weight.py +0 -0
  25. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/_support.py +0 -0
  26. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/__init__.py +0 -0
  27. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/_log_progress.py +0 -0
  28. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/base.py +0 -0
  29. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/pandas/__init__.py +0 -0
  30. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/pandas/_impl.py +0 -0
  31. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/polars/__init__.py +0 -0
  32. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/polars/_impl.py +0 -0
  33. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/integration/sklearn/__init__.py +0 -0
  34. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/py.typed +0 -0
  35. {time_split-0.4.0 → time_split-0.6.0}/src/time_split/support/types.py +0 -0
  36. {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.4.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 (>=3.1.0)
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
  [![Codecov](https://codecov.io/gh/rsundqvist/time-split/branch/master/graph/badge.svg)](https://codecov.io/gh/rsundqvist/time-split)
40
40
  [![Read the Docs](https://readthedocs.org/projects/time-split/badge/)](https://time-split.readthedocs.io/)
41
41
  [![PyPI - License](https://img.shields.io/pypi/l/time-split.svg)](https://pypi.python.org/pypi/time-split)
42
+ [![Docker Image Size (tag)](https://img.shields.io/docker/image-size/rsundqvist/time-split/latest?logo=docker&label=time-split)](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
+ [![Docker Image Size (tag)](https://img.shields.io/docker/image-size/rsundqvist/time-split/latest?logo=docker&label=time-split)](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
  [![Codecov](https://codecov.io/gh/rsundqvist/time-split/branch/master/graph/badge.svg)](https://codecov.io/gh/rsundqvist/time-split)
9
9
  [![Read the Docs](https://readthedocs.org/projects/time-split/badge/)](https://time-split.readthedocs.io/)
10
10
  [![PyPI - License](https://img.shields.io/pypi/l/time-split.svg)](https://pypi.python.org/pypi/time-split)
11
+ [![Docker Image Size (tag)](https://img.shields.io/docker/image-size/rsundqvist/time-split/latest?logo=docker&label=time-split)](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
+ [![Docker Image Size (tag)](https://img.shields.io/docker/image-size/rsundqvist/time-split/latest?logo=docker&label=time-split)](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.4.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
- 'Programming Language :: Python :: 3 :: Only',
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 = ">=3.1.0"
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
- numpy = "<2" # TODO: numpy 2.0
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.9.0"
89
- ruff = ">=0.3.7,<0.5.0"
88
+ mypy = "^1.10.1"
89
+ ruff = "^0.5.0"
90
90
 
91
- safety = "^3.1.0"
92
- codespell = "^2.2.6"
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/*" = [
@@ -11,6 +11,6 @@ __all__ = [
11
11
  "__version__", # Make MyPy happy
12
12
  ]
13
13
 
14
- __version__ = "0.4.0"
14
+ __version__ = "0.6.0"
15
15
 
16
16
  _logging.getLogger(__name__).addHandler(_logging.NullHandler())
@@ -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, Timestamp
6
+ from pandas import Timedelta
6
7
  from rics.misc import format_kwargs
7
8
 
8
- from time_split._compat import fmt_sec
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
- misc.snap_to_end
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
- return splits
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
- if original == expanded:
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
- def stringify(old: Timestamp, *, new: Timestamp) -> str:
135
- from .._frontend._to_string import _PrettyTimestamp
136
+ message = format_expanded_limits(original, expanded=expanded, expand_limits=self.expand_limits)
137
+ self._emit_once(message)
136
138
 
137
- retval = f"{old} -> "
138
- if old == new:
139
- return retval + "<no change>"
140
- diff = (new - old).total_seconds()
141
- return retval + f"{_PrettyTimestamp(new).auto} ({'+' if diff > 0 else '-'}{fmt_sec(abs(diff))})"
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: If ``True``, splits removed by `n_splits` or `step` are included in the figure.
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 = settings.DEFAULT_TIME_UNIT if plot_data.available is None else COUNT_ROWS
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
- _, ax = plt.subplots(
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="Outer range")
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, n_splits=0, step=1).get_plot_data(available)[0]
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
- return f"time_split.split({format_kwargs(kwargs, max_value_length=40)}{formatted_available})"
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 expand_limitsible bounds are enabled.
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 expand_limitsed limits do not pass sanity checks."""
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 = "h"
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