time-split 0.1.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 (33) hide show
  1. time_split-0.1.0/LICENSE.md +21 -0
  2. time_split-0.1.0/PKG-INFO +97 -0
  3. time_split-0.1.0/README.md +65 -0
  4. time_split-0.1.0/pyproject.toml +190 -0
  5. time_split-0.1.0/src/time_split/__init__.py +16 -0
  6. time_split-0.1.0/src/time_split/_backend/__init__.py +4 -0
  7. time_split-0.1.0/src/time_split/_backend/_available.py +57 -0
  8. time_split-0.1.0/src/time_split/_backend/_datetime_index_like.py +19 -0
  9. time_split-0.1.0/src/time_split/_backend/_limits.py +151 -0
  10. time_split-0.1.0/src/time_split/_backend/_schedule.py +66 -0
  11. time_split-0.1.0/src/time_split/_backend/_span.py +128 -0
  12. time_split-0.1.0/src/time_split/_backend/_splitter.py +162 -0
  13. time_split-0.1.0/src/time_split/_docstrings.py +36 -0
  14. time_split-0.1.0/src/time_split/_frontend/__init__.py +13 -0
  15. time_split-0.1.0/src/time_split/_frontend/_plot.py +355 -0
  16. time_split-0.1.0/src/time_split/_frontend/_progress.py +125 -0
  17. time_split-0.1.0/src/time_split/_frontend/_split.py +45 -0
  18. time_split-0.1.0/src/time_split/_frontend/_to_string.py +103 -0
  19. time_split-0.1.0/src/time_split/_frontend/_weight.py +66 -0
  20. time_split-0.1.0/src/time_split/_support.py +14 -0
  21. time_split-0.1.0/src/time_split/integration/__init__.py +1 -0
  22. time_split-0.1.0/src/time_split/integration/_log_progress.py +25 -0
  23. time_split-0.1.0/src/time_split/integration/pandas/__init__.py +42 -0
  24. time_split-0.1.0/src/time_split/integration/pandas/_impl.py +81 -0
  25. time_split-0.1.0/src/time_split/integration/polars/__init__.py +57 -0
  26. time_split-0.1.0/src/time_split/integration/polars/_impl.py +69 -0
  27. time_split-0.1.0/src/time_split/integration/sklearn/__init__.py +7 -0
  28. time_split-0.1.0/src/time_split/integration/sklearn/_impl.py +170 -0
  29. time_split-0.1.0/src/time_split/integration/split_data.py +72 -0
  30. time_split-0.1.0/src/time_split/py.typed +0 -0
  31. time_split-0.1.0/src/time_split/settings.py +172 -0
  32. time_split-0.1.0/src/time_split/support/__init__.py +21 -0
  33. time_split-0.1.0/src/time_split/types.py +61 -0
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2024, Richard Sundqvist
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.1
2
+ Name: time-split
3
+ Version: 0.1.0
4
+ Summary: My personal little ML engineering library.
5
+ Home-page: https://github.com/rsundqvist/time-split
6
+ Author: Richard Sundqvist
7
+ Author-email: richard.sundqvist@live.se
8
+ Requires-Python: >=3.11
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Provides-Extra: plotting
22
+ Requires-Dist: matplotlib ; extra == "plotting"
23
+ Requires-Dist: pandas (>=2.0.3)
24
+ Requires-Dist: rics (>=3.1.0)
25
+ Requires-Dist: seaborn ; extra == "plotting"
26
+ Project-URL: Bug Tracker, https://github.com/rsundqvist/time-split/issues
27
+ Project-URL: Changelog, https://github.com/rsundqvist/time-split/blob/master/CHANGELOG.md
28
+ Project-URL: Documentation, https://time-split.readthedocs.io
29
+ Project-URL: Repository, https://github.com/rsundqvist/time-split
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Time Split <!-- omit in toc -->
33
+ Flexible k-fold validation splits for heterogeneous data.
34
+
35
+ -----------------
36
+ [![PyPI - Version](https://img.shields.io/pypi/v/time-split.svg)](https://pypi.python.org/pypi/time-split)
37
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/time-split.svg)](https://pypi.python.org/pypi/time-split)
38
+ [![Tests](https://github.com/rsundqvist/time-split/workflows/tests/badge.svg)](https://github.com/rsundqvist/time-split/actions?workflow=tests)
39
+ [![Codecov](https://codecov.io/gh/rsundqvist/time-split/branch/master/graph/badge.svg)](https://codecov.io/gh/rsundqvist/time-split)
40
+ [![Read the Docs](https://readthedocs.org/projects/time-split/badge/)](https://time-split.readthedocs.io/)
41
+ [![PyPI - License](https://img.shields.io/pypi/l/time-split.svg)](https://pypi.python.org/pypi/time-split)
42
+
43
+ <div align="center">
44
+ <img alt="Plotted folds on a two-by-two grid."
45
+ title="Examples" height="300" width="1200"
46
+ src="https://raw.githubusercontent.com/rsundqvist/time-split/master/docs/2x2-examples.jpg"><br>
47
+ </div>
48
+
49
+ Plotted folds on a two-by-two grid. See the
50
+ [examples](https://time-split.readthedocs.io/en/stable/auto_examples/index.html) page for more.
51
+
52
+ ## What is it?
53
+ A library for creating cross-validation splits of _heterogeneous_ data, such as raw transaction data with strong
54
+ non-stationary characteristics.
55
+
56
+ ## Highlighted Features
57
+ - [Splitting schedules](https://time-split.readthedocs.io/en/stable/guide/schedules.html) based on a fixed interval,
58
+ a CRON-expression, or a pre-defined list.
59
+ - [Data selection](https://time-split.readthedocs.io/en/stable/guide/spans.html) based on a timedelta, or the splitting
60
+ schedule itself.
61
+ - Automatically extract and [normalize](https://time-split.readthedocs.io/en/stable/guide/flex.html) data limits for
62
+ supported data types.
63
+ - [Plotting function](https://time-split.readthedocs.io/en/stable/generated/time_split.html#time_split.plot) for
64
+ visualization of folds. Display fold sizes (hour/row count), or
65
+ [use custom text](https://time-split.readthedocs.io/en/stable/auto_examples/plotting_with_metrics.html).
66
+ - [Integrations](https://time-split.readthedocs.io/en/stable/generated/time_split.integration.html) for popular
67
+ libraries such as `pandas`, `polars` and `scikit-learn`.
68
+
69
+ ## Installation
70
+ The package is published through the [Python Package Index (PyPI)]. Source code
71
+ is available on GitHub: https://github.com/rsundqvist/time-split
72
+
73
+ ```sh
74
+ pip install -U time-split
75
+ ```
76
+
77
+ This is the preferred method to install ``time-split``, as it will always install the
78
+ most recent stable release.
79
+
80
+ If you don't have [pip] installed, this [Python installation guide] can guide
81
+ you through the process.
82
+
83
+ ## License
84
+ [MIT](LICENSE.md)
85
+
86
+ ## Documentation
87
+ Hosted on Read the Docs: https://time-split.readthedocs.io
88
+
89
+ ## Contributing
90
+
91
+ All contributions, bug reports, bug fixes, documentation improvements, enhancements, and ideas are welcome. To get
92
+ started, see the [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md).
93
+
94
+ [Python Package Index (PyPI)]: https://pypi.org/project/time-split
95
+ [pip]: https://pip.pypa.io
96
+ [Python installation guide]: http://docs.python-guide.org/en/stable/starting/installation/
97
+
@@ -0,0 +1,65 @@
1
+ # Time Split <!-- omit in toc -->
2
+ Flexible k-fold validation splits for heterogeneous data.
3
+
4
+ -----------------
5
+ [![PyPI - Version](https://img.shields.io/pypi/v/time-split.svg)](https://pypi.python.org/pypi/time-split)
6
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/time-split.svg)](https://pypi.python.org/pypi/time-split)
7
+ [![Tests](https://github.com/rsundqvist/time-split/workflows/tests/badge.svg)](https://github.com/rsundqvist/time-split/actions?workflow=tests)
8
+ [![Codecov](https://codecov.io/gh/rsundqvist/time-split/branch/master/graph/badge.svg)](https://codecov.io/gh/rsundqvist/time-split)
9
+ [![Read the Docs](https://readthedocs.org/projects/time-split/badge/)](https://time-split.readthedocs.io/)
10
+ [![PyPI - License](https://img.shields.io/pypi/l/time-split.svg)](https://pypi.python.org/pypi/time-split)
11
+
12
+ <div align="center">
13
+ <img alt="Plotted folds on a two-by-two grid."
14
+ title="Examples" height="300" width="1200"
15
+ src="https://raw.githubusercontent.com/rsundqvist/time-split/master/docs/2x2-examples.jpg"><br>
16
+ </div>
17
+
18
+ Plotted folds on a two-by-two grid. See the
19
+ [examples](https://time-split.readthedocs.io/en/stable/auto_examples/index.html) page for more.
20
+
21
+ ## What is it?
22
+ A library for creating cross-validation splits of _heterogeneous_ data, such as raw transaction data with strong
23
+ non-stationary characteristics.
24
+
25
+ ## Highlighted Features
26
+ - [Splitting schedules](https://time-split.readthedocs.io/en/stable/guide/schedules.html) based on a fixed interval,
27
+ a CRON-expression, or a pre-defined list.
28
+ - [Data selection](https://time-split.readthedocs.io/en/stable/guide/spans.html) based on a timedelta, or the splitting
29
+ schedule itself.
30
+ - Automatically extract and [normalize](https://time-split.readthedocs.io/en/stable/guide/flex.html) data limits for
31
+ supported data types.
32
+ - [Plotting function](https://time-split.readthedocs.io/en/stable/generated/time_split.html#time_split.plot) for
33
+ visualization of folds. Display fold sizes (hour/row count), or
34
+ [use custom text](https://time-split.readthedocs.io/en/stable/auto_examples/plotting_with_metrics.html).
35
+ - [Integrations](https://time-split.readthedocs.io/en/stable/generated/time_split.integration.html) for popular
36
+ libraries such as `pandas`, `polars` and `scikit-learn`.
37
+
38
+ ## Installation
39
+ The package is published through the [Python Package Index (PyPI)]. Source code
40
+ is available on GitHub: https://github.com/rsundqvist/time-split
41
+
42
+ ```sh
43
+ pip install -U time-split
44
+ ```
45
+
46
+ This is the preferred method to install ``time-split``, as it will always install the
47
+ most recent stable release.
48
+
49
+ If you don't have [pip] installed, this [Python installation guide] can guide
50
+ you through the process.
51
+
52
+ ## License
53
+ [MIT](LICENSE.md)
54
+
55
+ ## Documentation
56
+ Hosted on Read the Docs: https://time-split.readthedocs.io
57
+
58
+ ## Contributing
59
+
60
+ All contributions, bug reports, bug fixes, documentation improvements, enhancements, and ideas are welcome. To get
61
+ started, see the [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md).
62
+
63
+ [Python Package Index (PyPI)]: https://pypi.org/project/time-split
64
+ [pip]: https://pip.pypa.io
65
+ [Python installation guide]: http://docs.python-guide.org/en/stable/starting/installation/
@@ -0,0 +1,190 @@
1
+ [tool.poetry]
2
+ name = "time-split"
3
+ version = "0.1.0"
4
+ description = "My personal little ML engineering library."
5
+ authors = ["Richard Sundqvist <richard.sundqvist@live.se>"]
6
+
7
+ readme = "README.md"
8
+ homepage = "https://github.com/rsundqvist/time-split"
9
+ repository = "https://github.com/rsundqvist/time-split"
10
+ documentation = "https://time-split.readthedocs.io"
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "Intended Audience :: Science/Research",
15
+ "Operating System :: POSIX :: Linux",
16
+ "Operating System :: MacOS",
17
+ "Operating System :: Microsoft :: Windows",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Natural Language :: English",
20
+ "Programming Language :: Python :: 3",
21
+ 'Programming Language :: Python :: 3 :: Only',
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ ]
25
+
26
+
27
+ [tool.poetry.urls]
28
+ "Bug Tracker" = "https://github.com/rsundqvist/time-split/issues"
29
+ "Changelog" = "https://github.com/rsundqvist/time-split/blob/master/CHANGELOG.md"
30
+
31
+ [tool.poetry.scripts]
32
+
33
+ [tool.poetry.dependencies]
34
+ python = ">=3.11"
35
+
36
+ pandas = ">=2.0.3"
37
+ rics = ">=3.1.0"
38
+
39
+ # plotting
40
+ matplotlib = { version = "*", optional = true }
41
+ seaborn = { version = "*", optional = true }
42
+
43
+ [tool.poetry.extras]
44
+ plotting = ["matplotlib", "seaborn"]
45
+
46
+ [tool.poetry.group.manual-extras.dependencies]
47
+ # Extras that must be available for docs + tests go here. Doc/test-only deps go in their respective groups.
48
+ croniter = "^2.0.3"
49
+ pyarrow = "^15.0.2"
50
+ polars = { version = "^0.20.19", extras = ["pandas"] }
51
+
52
+ [tool.poetry.group.docs]
53
+ [tool.poetry.group.docs.dependencies]
54
+ # duplicate object warnings for sphinx>=4
55
+ # description: https://github.com/astropy/astropy/issues/11723
56
+ # https://github.com/astropy/astropy/pull/12270
57
+ sphinx = "^7.2.6"
58
+ sphinx-gallery = "^0.15.0"
59
+ pydata-sphinx-theme = "^0.15.2"
60
+ myst-parser = "^2.0.0"
61
+ nbsphinx = "^0.9.3"
62
+ ipython = "^8.23.0"
63
+
64
+ [tool.poetry.group.notebooks]
65
+ optional = true
66
+ [tool.poetry.group.notebooks.dependencies]
67
+ jupyterlab = "^4"
68
+ ipywidgets = "*"
69
+ jupyterlab-execute-time = "*"
70
+ tqdm = "*"
71
+
72
+ [tool.poetry.group.test.dependencies]
73
+ pytest = "^8.1.1"
74
+ xdoctest = "^1.1.3"
75
+ coverage = "^7.4.4"
76
+ pytest-cov = "^5.0.0"
77
+
78
+ scikit-learn = "^1.4.2"
79
+ # TODO Unpin.
80
+ # dask.datasets.timeseries() warns:
81
+ # UserWarning: dask_expr does not support the DataFrameIOFunction protocol for column projection
82
+ # on dask == 2024.3.1
83
+ dask = { version = "==2024.4.1", extras = ["dataframe"] }
84
+
85
+ [tool.poetry.group.devops.dependencies]
86
+ invoke = "^2.2.0"
87
+ mypy = "^1.9.0"
88
+ ruff = "^0.3.7"
89
+
90
+ safety = "^3.1.0"
91
+ codespell = "^2.2.6"
92
+ bump2version = "^1.0.1"
93
+
94
+ [tool.ruff]
95
+ line-length = 120
96
+ fix = true
97
+ unsafe-fixes = false
98
+ output-format = "grouped"
99
+
100
+ [tool.ruff.format]
101
+ docstring-code-format = true
102
+ docstring-code-line-length = 80
103
+ line-ending = "lf"
104
+
105
+ [tool.ruff.lint]
106
+ exclude = ["notebooks/*"]
107
+ select = [
108
+ "ANN",
109
+ "S",
110
+ "I",
111
+ "B",
112
+ "D",
113
+ "F",
114
+ "W",
115
+ "E",
116
+ "SLOT",
117
+ "SIM",
118
+ "TID",
119
+ "TD",
120
+ "TCH",
121
+ "ARG",
122
+ "PTH",
123
+ "NPY",
124
+ "PD",
125
+ "N",
126
+ "PERF",
127
+ "PLC",
128
+ "PLE",
129
+ "PLR",
130
+ "PLW",
131
+ "UP",
132
+ "FURB",
133
+ "RUF",
134
+ "TRY",
135
+ ]
136
+ ignore = [
137
+ "ANN101", # Missing type annotation for `self` in method
138
+ "ANN102", # Missing type annotation for `cls` in method
139
+ "D105", # Missing docstring in magic method
140
+ "D107", # Missing docstring in `__init__`
141
+ "ANN401",
142
+ "TID252", # Prefer absolute imports over relative imports from parent modules
143
+ "TRY003",
144
+ "PD901", # Avoid using the generic variable name `df` for DataFrames
145
+ "PLC0415", # `import` should be at the top-level of a file
146
+ "PLR0913", #
147
+ "SIM108", # Use ternary operator - coverage issue, see https://github.com/nedbat/coveragepy/issues/509
148
+ "TD003", # Missing issue link
149
+ ]
150
+
151
+ [tool.ruff.lint.per-file-ignores]
152
+ "tests/*" = [
153
+ "D",
154
+ "S101", # Allow 'assert' keyword
155
+ "ANN",
156
+ "PLR2004", # Allow magic values
157
+ ]
158
+ "examples/*" = [
159
+ "D205", # Clashes with sphinx_gallery (generated examples)
160
+ "B007",
161
+ ]
162
+ "**/time_split/*" = [
163
+ "PLR0913", # Too many arguments in function definition
164
+ ]
165
+ "**/time_split/integration/sklearn/*" = [
166
+ "N803", # Argument name `X` should be lowercase
167
+ "ARG002", # Unused method argument: `groups`
168
+ ]
169
+
170
+ [tool.ruff.lint.pydocstyle]
171
+ convention = "google"
172
+ [tool.ruff.lint.pycodestyle]
173
+ max-line-length = 150
174
+
175
+ [tool.pytest]
176
+ [tool.pytest.ini_options]
177
+ filterwarnings = [
178
+ "error",
179
+ "ignore:Matplotlib is currently using agg:UserWarning",
180
+ "ignore:.*utcfromtimestamp.*:DeprecationWarning", # Detta är farligt - ta bort
181
+ ]
182
+ log_file_level = 10
183
+ log_file = "pytest.log"
184
+ norecursedirs = [
185
+ "**/cli_modules/*",
186
+ ]
187
+
188
+ [build-system]
189
+ requires = ["poetry-core>=1.0.8"]
190
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,16 @@
1
+ """Create temporal k-folds for cross-validation with heterogeneous data."""
2
+
3
+ import logging as _logging
4
+
5
+ from ._frontend import log_split_progress, plot, split
6
+
7
+ __all__ = [
8
+ "log_split_progress",
9
+ "plot",
10
+ "split",
11
+ "__version__", # Make MyPy happy
12
+ ]
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ _logging.getLogger(__name__).addHandler(_logging.NullHandler())
@@ -0,0 +1,4 @@
1
+ from ._limits import expand_limits
2
+ from ._splitter import DatetimeIndexSplitter
3
+
4
+ __all__ = ["DatetimeIndexSplitter", "expand_limits"]
@@ -0,0 +1,57 @@
1
+ from typing import NamedTuple
2
+
3
+ from pandas import DatetimeIndex, Timestamp, isna
4
+
5
+ from .._support import handle_dask
6
+ from ..types import DatetimeIterable, Flex
7
+ from ._datetime_index_like import DatetimeIndexLike
8
+ from ._limits import LimitsTuple, expand_limits
9
+
10
+
11
+ class AvailableMetadata(NamedTuple):
12
+ """Output of :func:`process_available`."""
13
+
14
+ available_as_index: DatetimeIndexLike | None
15
+ limits: LimitsTuple
16
+ expanded_limits: LimitsTuple
17
+
18
+
19
+ def process_available(available: DatetimeIterable, *, flex: Flex) -> AvailableMetadata:
20
+ """Process a user-given `available` argument.
21
+
22
+ Args:
23
+ available: Available data from user. May be ``None``
24
+ flex: Flex-argument. Determines how much (if at all) to expand limits.
25
+
26
+ Returns:
27
+ A tuple ``(available, limits)``. Note that `available` will be ``None``, it has not been iterated over. This
28
+ assures that iterables are not consumed unless needed.
29
+
30
+ Raises:
31
+ ValueError: For invalid `available` arguments.
32
+
33
+ """
34
+ if isinstance(available, DatetimeIndexLike):
35
+ # Avoids conversion, which may be expensive (or impossible on the current machine),
36
+ # and leverages additional non-Pandas types that do vectorized min/max.
37
+ limits = _compute_data_limits(available)
38
+ available_retval = available
39
+ else:
40
+ available_retval = DatetimeIndex(available)
41
+ limits = _compute_data_limits(available_retval)
42
+
43
+ min_dt, max_dt = limits
44
+ if min_dt == max_dt or isna(min_dt) or isna(max_dt):
45
+ raise ValueError("Not enough data in 'available'; at least two unique elements are required.")
46
+
47
+ if not (isinstance(min_dt, Timestamp) and isinstance(max_dt, Timestamp)):
48
+ # Enforce pandas types; we might have a numpy.datetime64
49
+ # (croniter 1.4.1 + numpy 1.25.2): croniter cannot handle numpy.datetime64
50
+ limits = Timestamp(min_dt), Timestamp(max_dt)
51
+
52
+ expanded_limits = expand_limits(limits, flex=flex)
53
+ return AvailableMetadata(available_retval, limits=limits, expanded_limits=expanded_limits)
54
+
55
+
56
+ def _compute_data_limits(available: DatetimeIndexLike) -> LimitsTuple:
57
+ return handle_dask(available.min()), handle_dask(available.max())
@@ -0,0 +1,19 @@
1
+ from collections.abc import Iterable, Iterator
2
+ from typing import Protocol, runtime_checkable
3
+
4
+ from pandas import Series
5
+
6
+ from ..types import DatetimeTypes
7
+
8
+
9
+ @runtime_checkable
10
+ class DatetimeIndexLike(Protocol, Iterable[DatetimeTypes]):
11
+ """A type that behaves (sort of) like a pandas DatetimeIndex."""
12
+
13
+ def __iter__(self) -> Iterator[DatetimeTypes]: ...
14
+
15
+ def value_counts(self) -> Series: ...
16
+
17
+ def min(self) -> DatetimeTypes: ...
18
+
19
+ def max(self) -> DatetimeTypes: ...
@@ -0,0 +1,151 @@
1
+ from collections.abc import Iterable
2
+ from itertools import starmap
3
+ from typing import NamedTuple
4
+
5
+ from pandas import Timedelta, Timestamp
6
+
7
+ from ..settings import auto_flex
8
+ from ..types import Flex, TimedeltaTypes
9
+
10
+ LimitsTuple = tuple[Timestamp, Timestamp]
11
+ LevelTuple = tuple[TimedeltaTypes, TimedeltaTypes, TimedeltaTypes]
12
+
13
+
14
+ class _TimedeltaTuple(NamedTuple):
15
+ start_at: Timedelta
16
+ round_to: Timedelta
17
+ tolerance: Timedelta
18
+
19
+
20
+ def expand_limits(limits: LimitsTuple, *, flex: Flex | LevelTuple | Iterable[LevelTuple] = "auto") -> LimitsTuple:
21
+ """Derive the `"real"` bounds of `limits`.
22
+
23
+ Args:
24
+ limits: A tuple ``(lo, hi)`` of timestamps.
25
+ flex: Flex arguments as described in the :ref:`User guide`. Also supports level-tuples
26
+ ``[(start_at, round_to, tolerance)...]``. Passing ``flex=[settings.auto_flex.day, settings.auto_flex.hour]``
27
+ is equivalent to ``flex='auto'``.
28
+
29
+ Returns:
30
+ Limits rounded according to the `flex`-argument.
31
+
32
+ Raises:
33
+ ValueError: For invalid limits.
34
+
35
+ Examples:
36
+ >>> from pandas import Timestamp
37
+ >>> limits = Timestamp("2019-05-11"), Timestamp("2019-05-11 22:05:30")
38
+
39
+ Basic usage.
40
+
41
+ >>> expand_limits(limits, flex="d")
42
+ (Timestamp('2019-05-11 00:00:00'), Timestamp('2019-05-12 00:00:00'))
43
+
44
+ You may specify a maximum "distance" that limits may be expanded.
45
+
46
+ >>> expand_limits(limits, flex="d<1h")
47
+ (Timestamp('2019-05-11 00:00:00'), Timestamp('2019-05-11 22:05:30'))
48
+
49
+ Limits will never be rounded in the "wrong" direction...
50
+
51
+ >>> limits = Timestamp("2019-05-11"), Timestamp("2019-05-11 11:05:30")
52
+ >>> expand_limits(limits, flex="d")
53
+ (Timestamp('2019-05-11 00:00:00'), Timestamp('2019-05-11 11:05:30'))
54
+
55
+ ...even if you make the tolerance large enough.
56
+
57
+ >>> expand_limits(limits, flex="d<14h")
58
+ (Timestamp('2019-05-11 00:00:00'), Timestamp('2019-05-11 11:05:30'))
59
+
60
+ """
61
+ if limits[0] >= limits[1]:
62
+ msg = f"Bad limits. Expected limits[1] > limits[0], but got {limits=}."
63
+ raise ValueError(msg)
64
+
65
+ if flex is False:
66
+ return limits
67
+
68
+ if flex is True or flex == "auto":
69
+ return _from_levels(limits)
70
+
71
+ if isinstance(flex, str):
72
+ round_to, _, tolerance = flex.partition("<")
73
+ level = _make_level(None, round_to=round_to.strip(), tolerance=tolerance.strip())
74
+ return _apply(limits, level=level)
75
+ if isinstance(flex, tuple):
76
+ return _from_levels(limits, levels=[_make_level(*flex)])
77
+
78
+ return _from_levels(limits, levels=starmap(_make_level, flex))
79
+
80
+
81
+ def _from_levels(limits: LimitsTuple, *, levels: Iterable[_TimedeltaTuple] | None = None) -> LimitsTuple:
82
+ if levels is None:
83
+ levels = _levels_from_settings()
84
+ else:
85
+ levels = sorted(levels, reverse=True, key=lambda level: level[0])
86
+
87
+ lo, hi = limits
88
+ diff = hi - lo
89
+
90
+ for level in levels:
91
+ start_at, _round_to, _tolerance = level
92
+ if diff >= start_at:
93
+ return _apply(limits, level=level)
94
+
95
+ return limits
96
+
97
+
98
+ def _apply(limits: LimitsTuple, *, level: _TimedeltaTuple) -> LimitsTuple:
99
+ lo, hi = limits
100
+
101
+ lo_floor = lo.floor(level.round_to)
102
+ if abs(lo_floor - lo) > level.tolerance:
103
+ return limits
104
+
105
+ hi_ceil = hi.ceil(level.round_to)
106
+ if abs(hi_ceil - hi) > level.tolerance:
107
+ return limits
108
+
109
+ if auto_flex.SANITY_CHECK:
110
+ if lo.round(level.round_to) != lo_floor:
111
+ return limits
112
+ if hi.round(level.round_to) != hi_ceil:
113
+ return limits
114
+
115
+ return lo_floor, hi_ceil
116
+
117
+
118
+ def _levels_from_settings() -> list[_TimedeltaTuple]:
119
+ day, hour = _make_level(*auto_flex.day), _make_level(*auto_flex.hour)
120
+ if day.round_to != Timedelta(days=1):
121
+ msg = f"Invalid settings: {auto_flex.day=} must have round_to=1 day."
122
+ raise ValueError(msg)
123
+ if hour.round_to != Timedelta(hours=1):
124
+ msg = f"Invalid settings: {auto_flex.hour=} must have round_to=1 hour."
125
+ raise ValueError(msg)
126
+ return [day, hour]
127
+
128
+
129
+ def _make_level(
130
+ start_at: TimedeltaTypes | None,
131
+ round_to: TimedeltaTypes,
132
+ tolerance: TimedeltaTypes,
133
+ ) -> _TimedeltaTuple:
134
+ start_at = Timedelta(start_at) # Will be NaT if None, making all ifs False.
135
+ if start_at <= Timedelta(0):
136
+ raise ValueError(f"Bad {start_at=}; must be non-negative.")
137
+
138
+ try:
139
+ round_to = Timedelta(1, unit=round_to)
140
+ except ValueError as e:
141
+ raise ValueError(f"Got {round_to=}, which is not a valid frequency.") from e
142
+
143
+ tolerance = Timedelta(tolerance)
144
+ if tolerance <= Timedelta(0):
145
+ raise ValueError(f"Bad {tolerance=}; must be non-negative.")
146
+
147
+ if start_at < round_to:
148
+ raise ValueError(f"{start_at=} < {round_to=}")
149
+ if round_to < tolerance:
150
+ raise ValueError(f"{round_to=} < {tolerance=}")
151
+ return _TimedeltaTuple(start_at, round_to, tolerance)