time-split 0.1.0__py3-none-any.whl
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/__init__.py +16 -0
- time_split/_backend/__init__.py +4 -0
- time_split/_backend/_available.py +57 -0
- time_split/_backend/_datetime_index_like.py +19 -0
- time_split/_backend/_limits.py +151 -0
- time_split/_backend/_schedule.py +66 -0
- time_split/_backend/_span.py +128 -0
- time_split/_backend/_splitter.py +162 -0
- time_split/_docstrings.py +36 -0
- time_split/_frontend/__init__.py +13 -0
- time_split/_frontend/_plot.py +355 -0
- time_split/_frontend/_progress.py +125 -0
- time_split/_frontend/_split.py +45 -0
- time_split/_frontend/_to_string.py +103 -0
- time_split/_frontend/_weight.py +66 -0
- time_split/_support.py +14 -0
- time_split/integration/__init__.py +1 -0
- time_split/integration/_log_progress.py +25 -0
- time_split/integration/pandas/__init__.py +42 -0
- time_split/integration/pandas/_impl.py +81 -0
- time_split/integration/polars/__init__.py +57 -0
- time_split/integration/polars/_impl.py +69 -0
- time_split/integration/sklearn/__init__.py +7 -0
- time_split/integration/sklearn/_impl.py +170 -0
- time_split/integration/split_data.py +72 -0
- time_split/py.typed +0 -0
- time_split/settings.py +172 -0
- time_split/support/__init__.py +21 -0
- time_split/types.py +61 -0
- time_split-0.1.0.dist-info/LICENSE.md +21 -0
- time_split-0.1.0.dist-info/METADATA +97 -0
- time_split-0.1.0.dist-info/RECORD +34 -0
- time_split-0.1.0.dist-info/WHEEL +4 -0
- time_split-0.1.0.dist-info/entry_points.txt +0 -0
time_split/_support.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Protocol, TypeVar, runtime_checkable
|
|
2
|
+
|
|
3
|
+
T_co = TypeVar("T_co", covariant=True)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class _Computable(Protocol[T_co]):
|
|
8
|
+
def compute(self) -> T_co:
|
|
9
|
+
"""Compute results of a delayed Dask object."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def handle_dask(arg: T_co | _Computable[T_co]) -> T_co:
|
|
13
|
+
"""Run Dask compute function on a delayed object."""
|
|
14
|
+
return arg.compute() if isinstance(arg, _Computable) else arg
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Convenience functions and classes for common libraries."""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from typing import Any, Union
|
|
4
|
+
|
|
5
|
+
from .. import log_split_progress
|
|
6
|
+
from .._docstrings import add_docstrings
|
|
7
|
+
from ..types import DatetimeSplitBounds, DatetimeSplits
|
|
8
|
+
|
|
9
|
+
LogProgressArg = Union[str, bool, dict[str, Any], logging.Logger, logging.LoggerAdapter] # type: ignore[type-arg]
|
|
10
|
+
|
|
11
|
+
add_docstrings(log_progress="Controls logging of fold progress. See :func:`~.log_split_progress` for details.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def handle_log_progress_arg(
|
|
15
|
+
log_progress: LogProgressArg, *, splits: DatetimeSplits
|
|
16
|
+
) -> Iterable[DatetimeSplitBounds] | None:
|
|
17
|
+
"""Wrapper function for integrations."""
|
|
18
|
+
if log_progress is True:
|
|
19
|
+
return log_split_progress(splits)
|
|
20
|
+
elif isinstance(log_progress, (str, logging.Logger, logging.LoggerAdapter)):
|
|
21
|
+
return log_split_progress(splits, logger=log_progress)
|
|
22
|
+
elif isinstance(log_progress, dict):
|
|
23
|
+
return log_split_progress(splits, **log_progress)
|
|
24
|
+
|
|
25
|
+
return None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
r"""Integration with the Pandas library.
|
|
2
|
+
|
|
3
|
+
Examples:
|
|
4
|
+
Splitting a ``pandas.Series`` with :func:`split_pandas`.
|
|
5
|
+
|
|
6
|
+
>>> import pandas as pd
|
|
7
|
+
>>> index = pd.date_range("2022", "2022-1-10", freq="h")
|
|
8
|
+
>>> series = pd.Series(range(len(index)), index=index)
|
|
9
|
+
>>> series.sample(3, random_state=1999)
|
|
10
|
+
2022-01-04 00:00:00 72
|
|
11
|
+
2022-01-03 21:00:00 69
|
|
12
|
+
2022-01-04 14:00:00 86
|
|
13
|
+
dtype: int64
|
|
14
|
+
|
|
15
|
+
Series may only be split on the index. The `schedule` keyword argument is required, but `log_progress` is not.
|
|
16
|
+
|
|
17
|
+
>>> for fold in split_pandas(
|
|
18
|
+
... series, schedule="1d", log_progress="progress"
|
|
19
|
+
... ): # doctest: +SKIP
|
|
20
|
+
... print(
|
|
21
|
+
... f"Summary of fold {tuple(map(pd.Timestamp.isoformat, fold.bounds))}:"
|
|
22
|
+
... f"\n {fold.data.mean()=}"
|
|
23
|
+
... f"\n {fold.future_data.mean()=}",
|
|
24
|
+
... )
|
|
25
|
+
INFO:progress:Begin fold 1/2: ('2022-01-01' <= [schedule: '2022-01-08' (Saturday)] < '2022-01-09').
|
|
26
|
+
Summary of fold ('2022-01-01T00:00:00', '2022-01-08T00:00:00', '2022-01-09T00:00:00'):
|
|
27
|
+
fold.data.mean()=83.5
|
|
28
|
+
fold.future_data.mean()=179.5
|
|
29
|
+
INFO:progress:Finished fold 1/2 [schedule: '2022-01-08' (Saturday)] after 1ms. # doctest: +SKIP
|
|
30
|
+
INFO:progress:Begin fold 2/2: ('2022-01-02' <= [schedule: '2022-01-09' (Sunday)] < '2022-01-10').
|
|
31
|
+
Summary of fold ('2022-01-02T00:00:00', '2022-01-09T00:00:00', '2022-01-10T00:00:00'):
|
|
32
|
+
fold.data.mean()=107.5
|
|
33
|
+
fold.future_data.mean()=203.5
|
|
34
|
+
INFO:progress:Finished fold 2/2 [schedule: '2022-01-09' (Sunday)] after 873μs.
|
|
35
|
+
|
|
36
|
+
When splitting dataframes, you may optionally pass a `time_column` argument as well. By default, both frames and
|
|
37
|
+
series are split along the index.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from ._impl import PandasT, split_pandas
|
|
41
|
+
|
|
42
|
+
__all__ = ["PandasT", "split_pandas"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from collections.abc import Hashable, Iterable
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
from typing import Generic, TypeVar, Unpack
|
|
4
|
+
|
|
5
|
+
from pandas import DataFrame, DatetimeIndex, Series, Timestamp
|
|
6
|
+
from rics.misc import tname
|
|
7
|
+
|
|
8
|
+
from ..._docstrings import docs
|
|
9
|
+
from ...types import DatetimeIndexSplitterKwargs
|
|
10
|
+
from .._log_progress import LogProgressArg
|
|
11
|
+
from ..split_data import DatetimeSplit, split_data
|
|
12
|
+
|
|
13
|
+
PandasT = TypeVar("PandasT", Series, DataFrame)
|
|
14
|
+
"""A splittable pandas type."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@docs
|
|
18
|
+
def split_pandas(
|
|
19
|
+
data: PandasT,
|
|
20
|
+
time_column: Hashable = None,
|
|
21
|
+
*,
|
|
22
|
+
log_progress: LogProgressArg = False,
|
|
23
|
+
**kwargs: Unpack[DatetimeIndexSplitterKwargs],
|
|
24
|
+
) -> Iterable[DatetimeSplit[PandasT]]:
|
|
25
|
+
"""Split a pandas type.
|
|
26
|
+
|
|
27
|
+
This function splits indexed data (i.e. ``Series`` and ``DataFrame``, not the index itself. Use
|
|
28
|
+
:func:`time_split.split <time_split.split>` for pandas ``Index`` types, setting ``available=data.index``.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
data: A pandas data container type to split; either ``Series`` or a ``DataFrame``.
|
|
32
|
+
time_column: A column in `data` to split on. Use ``data`.index`` if ``None``.
|
|
33
|
+
log_progress: {log_progress}
|
|
34
|
+
**kwargs: {DatetimeIndexSplitterKwargs}
|
|
35
|
+
|
|
36
|
+
{USER_GUIDE}
|
|
37
|
+
|
|
38
|
+
Yields:
|
|
39
|
+
Tuples ``(data, future_data, bounds)``.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
TypeError: If `time_column` does not denote a datetime index-like field.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
indexer = _Indexer(time_column)
|
|
46
|
+
|
|
47
|
+
yield from split_data(
|
|
48
|
+
data,
|
|
49
|
+
log_progress=log_progress,
|
|
50
|
+
as_available=indexer.as_available,
|
|
51
|
+
select=indexer.select,
|
|
52
|
+
**kwargs,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _Indexer(Generic[PandasT]):
|
|
57
|
+
def __init__(self, time_column: Hashable | None) -> None:
|
|
58
|
+
self.time_column = time_column
|
|
59
|
+
|
|
60
|
+
def as_available(self, data: PandasT) -> Series | DatetimeIndex:
|
|
61
|
+
time = self._get_time(data)
|
|
62
|
+
|
|
63
|
+
first = time.iloc[0] if hasattr(time, "iloc") else time[0]
|
|
64
|
+
if isinstance(first, date):
|
|
65
|
+
return time
|
|
66
|
+
|
|
67
|
+
type_str = "data.index" if self.time_column is None else f"data[{self.time_column!r}]"
|
|
68
|
+
msg = f"Elements of {type_str} element are {tname(first)}, expected datetime-like."
|
|
69
|
+
raise TypeError(msg)
|
|
70
|
+
|
|
71
|
+
def _get_time(self, data: DataFrame) -> Series:
|
|
72
|
+
return data.index if self.time_column is None else data[self.time_column]
|
|
73
|
+
|
|
74
|
+
def select(self, data: PandasT, left: datetime, right: datetime) -> PandasT:
|
|
75
|
+
"""Select data based on the given bounds."""
|
|
76
|
+
time = self._get_time(data)
|
|
77
|
+
if isinstance(time, Series):
|
|
78
|
+
return data[time.between(left, right, inclusive="left")]
|
|
79
|
+
else:
|
|
80
|
+
# Index slicing is a lot faster than boolean masks (empirically, seems to be a factor ~10).
|
|
81
|
+
return data[left : right - Timestamp.resolution] # type: ignore[misc]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
r"""Integration with the Polars library.
|
|
2
|
+
|
|
3
|
+
Examples:
|
|
4
|
+
Splitting a ``polars.DataFrame`` with :func:`split_polars`.
|
|
5
|
+
|
|
6
|
+
>>> import polars as pl
|
|
7
|
+
>>> from datetime import date
|
|
8
|
+
>>> ts = pl.datetime_range(
|
|
9
|
+
... date.fromisoformat("2022-01-01"),
|
|
10
|
+
... date.fromisoformat("2022-01-10"),
|
|
11
|
+
... interval="1h",
|
|
12
|
+
... eager=True,
|
|
13
|
+
... )
|
|
14
|
+
>>> df = pl.DataFrame({"timestamp": ts, "ints": range(len(ts))})
|
|
15
|
+
>>> df.sample(5, seed=1999)
|
|
16
|
+
shape: (5, 2)
|
|
17
|
+
┌─────────────────────┬──────┐
|
|
18
|
+
│ timestamp ┆ ints │
|
|
19
|
+
│ --- ┆ --- │
|
|
20
|
+
│ datetime[μs] ┆ i64 │
|
|
21
|
+
╞═════════════════════╪══════╡
|
|
22
|
+
│ 2022-01-06 16:00:00 ┆ 136 │
|
|
23
|
+
│ 2022-01-08 17:00:00 ┆ 185 │
|
|
24
|
+
│ 2022-01-01 06:00:00 ┆ 6 │
|
|
25
|
+
│ 2022-01-01 05:00:00 ┆ 5 │
|
|
26
|
+
│ 2022-01-03 08:00:00 ┆ 56 │
|
|
27
|
+
└─────────────────────┴──────┘
|
|
28
|
+
|
|
29
|
+
To split the frame, pass it to ``split_polars`` along with the column to split on. The `schedule` keyword argument
|
|
30
|
+
is required, but `log_progress` is not.
|
|
31
|
+
|
|
32
|
+
>>> for fold in split_polars(
|
|
33
|
+
... df, schedule="1d", log_progress="progress", time_column="timestamp"
|
|
34
|
+
... ): # doctest: +SKIP
|
|
35
|
+
... print(
|
|
36
|
+
... f"Summary of fold {tuple(map(pd.Timestamp.isoformat, fold.bounds))}:"
|
|
37
|
+
... f"\n {fold.data['ints'].mean()=}"
|
|
38
|
+
... f"\n {fold.future_data['ints'].mean()=}",
|
|
39
|
+
... )
|
|
40
|
+
INFO:progress:Begin fold 1/2: '2022-01-01' <= [schedule: '2022-01-08' (Saturday)] < '2022-01-09'.
|
|
41
|
+
Summary of fold ('2022-01-01T00:00:00', '2022-01-08T00:00:00', '2022-01-09T00:00:00'):
|
|
42
|
+
fold.data['ints'].mean()=83.5
|
|
43
|
+
fold.future_data['ints'].mean()=179.5
|
|
44
|
+
INFO:progress:Finished fold 1/2: [schedule: '2022-01-08' (Saturday)] after 542μs.
|
|
45
|
+
INFO:progress:Begin fold 2/2: '2022-01-02' <= [schedule: '2022-01-09' (Sunday)] < '2022-01-10'.
|
|
46
|
+
Summary of fold ('2022-01-02T00:00:00', '2022-01-09T00:00:00', '2022-01-10T00:00:00'):
|
|
47
|
+
fold.data['ints'].mean()=107.5
|
|
48
|
+
fold.future_data['ints'].mean()=203.5
|
|
49
|
+
INFO:progress:Finished fold 2/2: [schedule: '2022-01-09' (Sunday)] after 433μs.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from ._impl import split_polars
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"split_polars",
|
|
57
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
from typing import Unpack
|
|
4
|
+
|
|
5
|
+
from polars import DataFrame, Series
|
|
6
|
+
from rics.misc import tname
|
|
7
|
+
|
|
8
|
+
from ..._docstrings import docs
|
|
9
|
+
from ...types import DatetimeIndexSplitterKwargs
|
|
10
|
+
from .._log_progress import LogProgressArg
|
|
11
|
+
from ..split_data import DatetimeSplit, split_data
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@docs
|
|
15
|
+
def split_polars(
|
|
16
|
+
data: DataFrame,
|
|
17
|
+
time_column: str,
|
|
18
|
+
*,
|
|
19
|
+
log_progress: LogProgressArg = False,
|
|
20
|
+
**kwargs: Unpack[DatetimeIndexSplitterKwargs],
|
|
21
|
+
) -> Iterable[DatetimeSplit[DataFrame]]:
|
|
22
|
+
"""Split a polars frame.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
data: A ``polars.DataFrame``.
|
|
26
|
+
time_column: A column to split on.
|
|
27
|
+
log_progress: {log_progress}
|
|
28
|
+
**kwargs: {DatetimeIndexSplitterKwargs}
|
|
29
|
+
|
|
30
|
+
{USER_GUIDE}
|
|
31
|
+
|
|
32
|
+
Yields:
|
|
33
|
+
Tuples ``(data, future_data, bounds)``.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
TypeError: If `time_column` does not denote a datetime index-like field.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
indexer = _Indexer(time_column)
|
|
40
|
+
|
|
41
|
+
yield from split_data(
|
|
42
|
+
data,
|
|
43
|
+
log_progress=log_progress,
|
|
44
|
+
as_available=indexer.as_available,
|
|
45
|
+
select=indexer.select,
|
|
46
|
+
**kwargs,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _Indexer:
|
|
51
|
+
def __init__(self, time_column: str) -> None:
|
|
52
|
+
self.time_column = time_column
|
|
53
|
+
|
|
54
|
+
def as_available(self, data: DataFrame) -> Series:
|
|
55
|
+
time = self._get_time(data)
|
|
56
|
+
|
|
57
|
+
if isinstance(time[0], date):
|
|
58
|
+
return time
|
|
59
|
+
|
|
60
|
+
msg = f"Elements of data[{self.time_column!r}] element are {tname(time[0])}, expected datetime-like."
|
|
61
|
+
raise TypeError(msg)
|
|
62
|
+
|
|
63
|
+
def _get_time(self, data: DataFrame) -> Series:
|
|
64
|
+
return data[self.time_column]
|
|
65
|
+
|
|
66
|
+
def select(self, data: DataFrame, left: datetime, right: datetime) -> DataFrame:
|
|
67
|
+
"""Select data based on the given bounds."""
|
|
68
|
+
time = self._get_time(data)
|
|
69
|
+
return data.filter(time.is_between(left, right, closed="left"))
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from collections.abc import Iterable, Sequence
|
|
2
|
+
from typing import Any, cast, get_args
|
|
3
|
+
|
|
4
|
+
from numpy import array, datetime64, logical_and, ndarray, nonzero
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
|
|
7
|
+
from ..._backend import DatetimeIndexSplitter
|
|
8
|
+
from ..._docstrings import docs
|
|
9
|
+
from ...types import (
|
|
10
|
+
DatetimeIterable,
|
|
11
|
+
DatetimeSplits,
|
|
12
|
+
DatetimeTypes,
|
|
13
|
+
Flex,
|
|
14
|
+
Schedule,
|
|
15
|
+
Span,
|
|
16
|
+
)
|
|
17
|
+
from .._log_progress import LogProgressArg, handle_log_progress_arg
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from sklearn.model_selection import BaseCrossValidator # type: ignore[import-untyped]
|
|
21
|
+
|
|
22
|
+
except ModuleNotFoundError:
|
|
23
|
+
BaseCrossValidator = object
|
|
24
|
+
|
|
25
|
+
IndexTuple = tuple[Sequence[int], Sequence[int]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@docs
|
|
29
|
+
class ScikitLearnSplitter(BaseCrossValidator): # type: ignore[misc]
|
|
30
|
+
"""A scikit-learn compatible datetime splitter.
|
|
31
|
+
|
|
32
|
+
This class may be used to create temporal folds from heterogeneous/unaggregated data, typically used for training
|
|
33
|
+
models (e.g. on raw transaction data). If your data is a well-formed time series, consider using the
|
|
34
|
+
`TimeSeriesSplit`_ class from scikit-learn instead.
|
|
35
|
+
|
|
36
|
+
.. _TimeSeriesSplit: https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html
|
|
37
|
+
|
|
38
|
+
If a ``pandas`` type is passed to the :meth:`ScikitLearnSplitter.split`-method, the index will be used.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
schedule: {schedule}
|
|
42
|
+
before: {before}
|
|
43
|
+
after: {after}
|
|
44
|
+
step: {step}
|
|
45
|
+
n_splits: {n_splits}
|
|
46
|
+
flex: {flex}
|
|
47
|
+
log_progress: {log_progress}
|
|
48
|
+
verify_xy: If ``True``, split X and y independently and verify that they are equal.
|
|
49
|
+
|
|
50
|
+
{USER_GUIDE}
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
schedule: Schedule,
|
|
57
|
+
*,
|
|
58
|
+
before: Span = "7d",
|
|
59
|
+
after: Span = 1,
|
|
60
|
+
n_splits: int | None = None,
|
|
61
|
+
flex: Flex = "auto",
|
|
62
|
+
step: int = 1,
|
|
63
|
+
log_progress: LogProgressArg = False,
|
|
64
|
+
verify_xy: bool = True,
|
|
65
|
+
) -> None:
|
|
66
|
+
super().__init__()
|
|
67
|
+
self._splitter = DatetimeIndexSplitter(
|
|
68
|
+
schedule,
|
|
69
|
+
before=before,
|
|
70
|
+
after=after,
|
|
71
|
+
step=step,
|
|
72
|
+
n_splits=n_splits,
|
|
73
|
+
flex=flex,
|
|
74
|
+
)
|
|
75
|
+
self.log_progress = log_progress
|
|
76
|
+
self.verify_xy = verify_xy
|
|
77
|
+
|
|
78
|
+
def get_n_splits(
|
|
79
|
+
self, X: DatetimeIterable | None = None, y: DatetimeIterable | None = None, groups: Any = None
|
|
80
|
+
) -> int:
|
|
81
|
+
"""Returns the number of splitting iterations in the cross-validator.
|
|
82
|
+
|
|
83
|
+
Equivalent to ``len(list(split(X, y, groups))``.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
X: Training data (features).
|
|
87
|
+
y: Target variable.
|
|
88
|
+
groups: Always ignored, exists for compatibility.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Number of splits with given arguments.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If both `X` and `y` are ``None``.
|
|
95
|
+
ValueError: If splits of `X` and `y` are not equal when ``verify_xy=True``.
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
_, splits = self._get_splits(X, y)
|
|
99
|
+
return len(splits)
|
|
100
|
+
|
|
101
|
+
def split(
|
|
102
|
+
self,
|
|
103
|
+
X: DatetimeIterable | None = None,
|
|
104
|
+
y: DatetimeIterable | None = None,
|
|
105
|
+
groups: Any = None,
|
|
106
|
+
) -> Iterable[IndexTuple]:
|
|
107
|
+
"""Generate indices to split data into training and test set.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
X: Training data (features).
|
|
111
|
+
y: Target variable.
|
|
112
|
+
groups: Always ignored, exists for compatibility.
|
|
113
|
+
|
|
114
|
+
Yields:
|
|
115
|
+
The training/test set indices for that split.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If both `X` and `y` are ``None``.
|
|
119
|
+
ValueError: If splits of `X` and `y` are not equal when ``verify_xy=True``.
|
|
120
|
+
TypeError: If `X` or `y` have an ``index``-attribute, but index elements are not datetime-like.
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
index, splits = self._get_splits(self._handle_pandas(X, "X"), self._handle_pandas(y, "y"))
|
|
124
|
+
for fold in handle_log_progress_arg(self.log_progress, splits=splits) or splits:
|
|
125
|
+
yield cast(
|
|
126
|
+
IndexTuple,
|
|
127
|
+
(
|
|
128
|
+
nonzero(logical_and(fold.start <= index, index < fold.mid))[0],
|
|
129
|
+
nonzero(logical_and(fold.mid <= index, index < fold.end))[0],
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _handle_pandas(arg: DatetimeIterable | None, name: str) -> DatetimeIterable | None:
|
|
135
|
+
if arg is None or not hasattr(arg, "index"):
|
|
136
|
+
return arg
|
|
137
|
+
|
|
138
|
+
index = arg.index
|
|
139
|
+
|
|
140
|
+
if type(arg.index[0]) in get_args(DatetimeTypes):
|
|
141
|
+
return cast(DatetimeIterable, index)
|
|
142
|
+
|
|
143
|
+
raise TypeError(f"{name}.index does not appear to be a datetime-like iterable.")
|
|
144
|
+
|
|
145
|
+
def _get_splits(
|
|
146
|
+
self,
|
|
147
|
+
X: DatetimeIterable | None = None,
|
|
148
|
+
y: DatetimeIterable | None = None,
|
|
149
|
+
) -> tuple[NDArray[datetime64], DatetimeSplits]:
|
|
150
|
+
splits: DatetimeSplits | None = None
|
|
151
|
+
y_splits: DatetimeSplits | None = None
|
|
152
|
+
timestamps: Any | None = None
|
|
153
|
+
|
|
154
|
+
if X is not None:
|
|
155
|
+
splits = self._splitter.get_splits(X)
|
|
156
|
+
timestamps = X
|
|
157
|
+
if y is not None:
|
|
158
|
+
y_splits = self._splitter.get_splits(y)
|
|
159
|
+
timestamps = y
|
|
160
|
+
if self.verify_xy and splits and y_splits and splits != y_splits:
|
|
161
|
+
raise ValueError("Splits of X and y are not equal.")
|
|
162
|
+
|
|
163
|
+
# Cast should not be needed, but MyPy things this is nullable.
|
|
164
|
+
if splits is None and y_splits is None:
|
|
165
|
+
raise ValueError("At least one of (X, y) must be given.")
|
|
166
|
+
|
|
167
|
+
timestamps = timestamps if isinstance(timestamps, ndarray) else array(timestamps)
|
|
168
|
+
if len(timestamps.shape) > 1:
|
|
169
|
+
raise NotImplementedError(f"shape {timestamps.shape} not supported")
|
|
170
|
+
return timestamps, cast(DatetimeSplits, splits or y_splits)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Base implementations for splitting generic data types.
|
|
2
|
+
|
|
3
|
+
Users may implement splitting of any data type by implementing suitable ``as_available`` and ``select`` functions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import typing as _t
|
|
7
|
+
from datetime import datetime as _datetime
|
|
8
|
+
|
|
9
|
+
from .. import _frontend, types
|
|
10
|
+
from .._docstrings import docs as _docs
|
|
11
|
+
from . import _log_progress
|
|
12
|
+
|
|
13
|
+
DataT = _t.TypeVar("DataT")
|
|
14
|
+
"""Type of data to split."""
|
|
15
|
+
|
|
16
|
+
DataAsAvailableFn = _t.Callable[[DataT], types.DatetimeIterable]
|
|
17
|
+
"""A callable ``(data: DataT) -> DatetimeIterable``."""
|
|
18
|
+
DataSelectFn = _t.Callable[[DataT, _datetime, _datetime], DataT]
|
|
19
|
+
"""A callable ``(data: DataT, left_inclusive: datetime, end_exclusive: datetime) -> DataT)``."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DatetimeSplit(_t.NamedTuple, _t.Generic[DataT]):
|
|
23
|
+
"""Time-based split of a generic data type."""
|
|
24
|
+
|
|
25
|
+
data: DataT
|
|
26
|
+
"""Data before ``bounds.mid``."""
|
|
27
|
+
future_data: DataT
|
|
28
|
+
"""Data after ``bounds.mid``."""
|
|
29
|
+
bounds: types.DatetimeSplitBounds
|
|
30
|
+
"""The underlying bounds that produced this split."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@_docs
|
|
34
|
+
def split_data(
|
|
35
|
+
data: DataT,
|
|
36
|
+
*,
|
|
37
|
+
log_progress: _log_progress.LogProgressArg = False,
|
|
38
|
+
as_available: DataAsAvailableFn[DataT],
|
|
39
|
+
select: DataSelectFn[DataT],
|
|
40
|
+
**kwargs: _t.Unpack[types.DatetimeIndexSplitterKwargs],
|
|
41
|
+
) -> _t.Iterable[DatetimeSplit[DataT]]:
|
|
42
|
+
"""Base implementation for splitting integrated `data` types.
|
|
43
|
+
|
|
44
|
+
The required ``as_available`` and ``select`` callables provided perform the actual integration.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
data: The data to split.
|
|
48
|
+
log_progress: {log_progress}
|
|
49
|
+
as_available: A callable ``(data: DataT) -> DatetimeIterable``.
|
|
50
|
+
select: A callable ``(data: DataT, left_inclusive: datetime, end_exclusive: datetime) -> DataT)``.
|
|
51
|
+
**kwargs: Keyword arguments for :func:`.split`-function.
|
|
52
|
+
|
|
53
|
+
Yields:
|
|
54
|
+
Tuples ``(data, future_data, bounds)``.
|
|
55
|
+
|
|
56
|
+
See Also:
|
|
57
|
+
To get started with your own integration, copy :func:`~time_split.integration.pandas.split_pandas` or
|
|
58
|
+
:func:`~time_split.integration.polars.split_polars` and use it as the baseline (click ``[source]``) on
|
|
59
|
+
the linked function.
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
available = as_available(data)
|
|
63
|
+
splits = _frontend.split(**kwargs, available=available)
|
|
64
|
+
|
|
65
|
+
tracked_splits = _log_progress.handle_log_progress_arg(log_progress, splits=splits)
|
|
66
|
+
|
|
67
|
+
for bounds in splits if tracked_splits is None else tracked_splits:
|
|
68
|
+
yield DatetimeSplit(
|
|
69
|
+
select(data, bounds.start, bounds.mid),
|
|
70
|
+
future_data=select(data, bounds.mid, bounds.end),
|
|
71
|
+
bounds=bounds,
|
|
72
|
+
)
|
time_split/py.typed
ADDED
|
File without changes
|