tscp 0.1.0.dev0__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.
Files changed (72) hide show
  1. tscp/__init__.py +45 -0
  2. tscp/data/__init__.py +19 -0
  3. tscp/data/archive.py +488 -0
  4. tscp/data/availability.py +55 -0
  5. tscp/data/io.py +109 -0
  6. tscp/data/splits.py +169 -0
  7. tscp/datasets/__init__.py +6 -0
  8. tscp/datasets/synthetic.py +177 -0
  9. tscp/features/__init__.py +22 -0
  10. tscp/features/base.py +64 -0
  11. tscp/features/calendar.py +105 -0
  12. tscp/features/combine.py +69 -0
  13. tscp/features/context.py +42 -0
  14. tscp/features/history.py +58 -0
  15. tscp/features/phase.py +41 -0
  16. tscp/imports.py +32 -0
  17. tscp/methods/__init__.py +48 -0
  18. tscp/methods/aci.py +44 -0
  19. tscp/methods/base.py +228 -0
  20. tscp/methods/compose.py +90 -0
  21. tscp/methods/engine.py +128 -0
  22. tscp/methods/io.py +106 -0
  23. tscp/methods/nexcp.py +42 -0
  24. tscp/methods/split.py +30 -0
  25. tscp/metrics/__init__.py +30 -0
  26. tscp/metrics/conditional.py +66 -0
  27. tscp/metrics/interval.py +75 -0
  28. tscp/metrics/report.py +135 -0
  29. tscp/nn/__init__.py +13 -0
  30. tscp/nn/data.py +85 -0
  31. tscp/nn/losses.py +63 -0
  32. tscp/nn/training.py +125 -0
  33. tscp/online/__init__.py +6 -0
  34. tscp/online/aci.py +53 -0
  35. tscp/online/base.py +67 -0
  36. tscp/py.typed +0 -0
  37. tscp/quantile/__init__.py +16 -0
  38. tscp/quantile/base.py +73 -0
  39. tscp/quantile/distribution.py +170 -0
  40. tscp/quantile/weighted.py +103 -0
  41. tscp/regions/__init__.py +15 -0
  42. tscp/regions/base.py +139 -0
  43. tscp/regions/box.py +82 -0
  44. tscp/regions/interval.py +62 -0
  45. tscp/registry.py +179 -0
  46. tscp/scores/__init__.py +9 -0
  47. tscp/scores/absolute.py +22 -0
  48. tscp/scores/base.py +87 -0
  49. tscp/scores/cqr.py +71 -0
  50. tscp/scores/normalized.py +74 -0
  51. tscp/scores/signed.py +46 -0
  52. tscp/sources/__init__.py +7 -0
  53. tscp/sources/base.py +32 -0
  54. tscp/sources/expanding.py +25 -0
  55. tscp/sources/holdout.py +18 -0
  56. tscp/testing/__init__.py +40 -0
  57. tscp/testing/archives.py +80 -0
  58. tscp/testing/checks.py +832 -0
  59. tscp/testing/components.py +28 -0
  60. tscp/typing.py +24 -0
  61. tscp/utils/__init__.py +14 -0
  62. tscp/utils/component.py +76 -0
  63. tscp/utils/config.py +135 -0
  64. tscp/utils/hashing.py +35 -0
  65. tscp/weights/__init__.py +7 -0
  66. tscp/weights/base.py +22 -0
  67. tscp/weights/exponential.py +37 -0
  68. tscp/weights/uniform.py +23 -0
  69. tscp-0.1.0.dev0.dist-info/METADATA +130 -0
  70. tscp-0.1.0.dev0.dist-info/RECORD +72 -0
  71. tscp-0.1.0.dev0.dist-info/WHEEL +4 -0
  72. tscp-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,28 @@
1
+ """Minimal components for exercising code paths in checks and tests."""
2
+
3
+ from torch import Tensor
4
+
5
+ from tscp.data import ResidualArchive
6
+ from tscp.regions import Box
7
+ from tscp.scores import Score
8
+
9
+
10
+ class MaxAbsScore(Score):
11
+ """The largest absolute residual over the horizon: one joint box per series.
12
+
13
+ A minimal score that reduces the step dimension. Checking a method with it shows that
14
+ the method handles scores over the whole horizon, including their release after the
15
+ last step and their validity under missing targets.
16
+ """
17
+
18
+ reduce_dims = ("step",)
19
+
20
+ def forward(self, rows: ResidualArchive) -> Tensor:
21
+ return rows.residuals.abs().amax(dim=1)
22
+
23
+ def region(self, threshold: Tensor, rows: ResidualArchive) -> Box:
24
+ bound = threshold.unsqueeze(-1).expand(*threshold.shape, rows.horizon)
25
+ return Box(-bound, bound, event_dims=("step",))
26
+
27
+
28
+ __all__ = ["MaxAbsScore"]
tscp/typing.py ADDED
@@ -0,0 +1,24 @@
1
+ """Type aliases shared across tscp.
2
+
3
+ Shape conventions used in docstrings:
4
+
5
+ * ``T``: forecast origins (rows of a residual archive);
6
+ * ``H``: horizon steps;
7
+ * ``N``: series;
8
+ * ``P``: residuals in a calibration pool;
9
+ * ``Q``: query origins, one region each;
10
+ * ``*kept``: the dimensions of ``[H, N]`` that a score keeps (see
11
+ :attr:`tscp.scores.Score.reduce_dims`).
12
+ """
13
+
14
+ from typing import TypeAlias
15
+
16
+ import torch
17
+ from torch import Tensor
18
+
19
+ Alpha: TypeAlias = float | Tensor
20
+ """Miscoverage level: a float, or a tensor that broadcasts against the thresholds."""
21
+
22
+ Device: TypeAlias = str | torch.device
23
+
24
+ __all__ = ["Alpha", "Device", "Tensor"]
tscp/utils/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """Shared helpers: the component base class, configs and fingerprints."""
2
+
3
+ from .component import Component
4
+ from .config import ConfigMixin, from_config, qualified_name
5
+ from .hashing import array_fingerprint, config_hash
6
+
7
+ __all__ = [
8
+ "Component",
9
+ "ConfigMixin",
10
+ "array_fingerprint",
11
+ "config_hash",
12
+ "from_config",
13
+ "qualified_name",
14
+ ]
@@ -0,0 +1,76 @@
1
+ """The base class shared by every axis component."""
2
+
3
+ from typing import TYPE_CHECKING, Self
4
+
5
+ import torch
6
+
7
+ from tscp.utils.config import ConfigMixin
8
+
9
+ if TYPE_CHECKING:
10
+ from tscp.data import ResidualArchive
11
+
12
+
13
+ class Component(ConfigMixin, torch.nn.Module):
14
+ """A torch module with a recorded config and an optional fit step.
15
+
16
+ Fitted tensors are registered as parameters or buffers, so they live in
17
+ ``state_dict()`` and move with ``.to(device)``. Settings live in the config
18
+ (:meth:`~tscp.utils.config.ConfigMixin.to_config`).
19
+ """
20
+
21
+ #: Whether loading a saved method fits this component again, on the fit split of the
22
+ #: archive it was fitted on, because its fitted state lives outside torch (a forest,
23
+ #: say) and is not in ``state_dict()``.
24
+ refit_on_load: bool = False
25
+
26
+ def fit(self, train: "ResidualArchive", validation: "ResidualArchive | None" = None) -> Self:
27
+ """Fit trainable state.
28
+
29
+ The composer calls this only for components that override it. ``train`` is the
30
+ component-fitting split, ``archive.split("fit")``; ``validation`` is the
31
+ ``validation`` split when the plan has one, for early stopping and model choice.
32
+ Both are row subsets whose timeline ends where the split ends: the history before
33
+ and within the split is there, and nothing after it.
34
+ """
35
+ return self
36
+
37
+ @property
38
+ def is_trainable(self) -> bool:
39
+ """Whether :meth:`fit` does anything for this component."""
40
+ return type(self).fit is not Component.fit
41
+
42
+ def _load_from_state_dict(
43
+ self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, errors
44
+ ):
45
+ # Fitted buffers start as None, and their shape depends on the data, so a fresh
46
+ # component cannot hold the saved values yet: make room for them, then load.
47
+ for name, buffer in list(self._buffers.items()):
48
+ key = prefix + name
49
+ if name in self._non_persistent_buffers_set or key not in state_dict:
50
+ continue
51
+ incoming = state_dict[key]
52
+ if buffer is None or buffer.shape != incoming.shape:
53
+ device = buffer.device if buffer is not None else _device_of(self, incoming)
54
+ self._buffers[name] = torch.empty_like(incoming, device=device)
55
+ super()._load_from_state_dict(
56
+ state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, errors
57
+ )
58
+
59
+ def extra_repr(self) -> str:
60
+ config = self.__dict__.get("_config_arguments", {})
61
+ # Submodules print themselves; None means "use the default", shown as a submodule.
62
+ return ", ".join(
63
+ f"{name}={value!r}"
64
+ for name, value in config.items()
65
+ if value is not None and not isinstance(value, torch.nn.Module)
66
+ )
67
+
68
+
69
+ def _device_of(module: torch.nn.Module, fallback: torch.Tensor) -> torch.device:
70
+ """The device of a module's first tensor, or of ``fallback`` if it holds none."""
71
+ for tensor in (*module.parameters(recurse=False), *module.buffers(recurse=False)):
72
+ return tensor.device
73
+ return fallback.device
74
+
75
+
76
+ __all__ = ["Component"]
tscp/utils/config.py ADDED
@@ -0,0 +1,135 @@
1
+ """Constructor-argument configs, so presets and components rebuild from JSON.
2
+
3
+ Every subclass of :class:`ConfigMixin` records the arguments its constructor was called
4
+ with. :meth:`ConfigMixin.to_config` turns them into a JSON-serializable dict, nesting
5
+ components as ``{"class": ..., "args": ...}``, and :func:`from_config` rebuilds the object.
6
+ Only classes that subclass :class:`ConfigMixin` can be rebuilt, so a config cannot import
7
+ arbitrary code.
8
+ """
9
+
10
+ import functools
11
+ import importlib
12
+ import inspect
13
+ from collections.abc import Mapping
14
+ from typing import Any
15
+
16
+ _CLASSES: dict[str, type] = {}
17
+
18
+
19
+ def qualified_name(cls: type) -> str:
20
+ """Return ``module.QualifiedName``, the key a config uses for ``cls``."""
21
+ return f"{cls.__module__}.{cls.__qualname__}"
22
+
23
+
24
+ def _record_arguments(cls: type, signature: inspect.Signature, args, kwargs) -> dict[str, Any]:
25
+ bound = signature.bind(None, *args, **kwargs)
26
+ bound.apply_defaults()
27
+ arguments = dict(bound.arguments)
28
+ arguments.pop(next(iter(signature.parameters))) # self
29
+ for name, parameter in signature.parameters.items():
30
+ if parameter.kind is inspect.Parameter.VAR_KEYWORD:
31
+ arguments.update(arguments.pop(name, {}))
32
+ elif parameter.kind is inspect.Parameter.VAR_POSITIONAL and arguments.pop(name, ()):
33
+ raise TypeError(f"{cls.__name__} was given *args, which a config cannot record")
34
+ return arguments
35
+
36
+
37
+ class ConfigMixin:
38
+ """Record constructor arguments and rebuild objects from them.
39
+
40
+ The config is the constructor call, not the current state: changing an attribute after
41
+ construction does not change it. Fitted state belongs in ``state_dict()``.
42
+ """
43
+
44
+ def __init_subclass__(cls, **kwargs):
45
+ super().__init_subclass__(**kwargs)
46
+ _CLASSES[qualified_name(cls)] = cls
47
+ init = cls.__dict__.get("__init__")
48
+ if init is None or getattr(init, "_records_config", False):
49
+ return
50
+ signature = inspect.signature(init)
51
+
52
+ @functools.wraps(init)
53
+ def __init__(self, *args, **kwargs):
54
+ # The most derived constructor runs first; parents called through super() must
55
+ # not overwrite its record.
56
+ if "_config_arguments" not in self.__dict__:
57
+ arguments = _record_arguments(cls, signature, args, kwargs)
58
+ object.__setattr__(self, "_config_arguments", arguments)
59
+ init(self, *args, **kwargs)
60
+
61
+ __init__._records_config = True
62
+ cls.__init__ = __init__
63
+
64
+ def get_config(self) -> dict[str, Any]:
65
+ """Return the constructor arguments as a JSON-serializable dict."""
66
+ arguments = self.__dict__.get("_config_arguments", {})
67
+ return {name: _encode(value, name) for name, value in arguments.items()}
68
+
69
+ def to_config(self) -> dict[str, Any]:
70
+ """Return ``{"class": ..., "args": ...}``, which :func:`from_config` rebuilds."""
71
+ return {"class": qualified_name(type(self)), "args": self.get_config()}
72
+
73
+
74
+ def _encode(value: Any, name: str) -> Any:
75
+ if value is None or isinstance(value, bool | int | float | str):
76
+ return value
77
+ if isinstance(value, ConfigMixin):
78
+ return value.to_config()
79
+ if isinstance(value, list | tuple):
80
+ return [_encode(item, name) for item in value]
81
+ if isinstance(value, Mapping) and all(isinstance(key, str) for key in value):
82
+ return {key: _encode(item, f"{name}.{key}") for key, item in value.items()}
83
+ raise TypeError(
84
+ f"argument {name!r} has type {type(value).__name__}, which a config cannot record; "
85
+ "pass numbers, strings, lists, dicts or tscp components"
86
+ )
87
+
88
+
89
+ def _decode(value: Any) -> Any:
90
+ if isinstance(value, Mapping):
91
+ if set(value) == {"class", "args"}:
92
+ return from_config(value)
93
+ return {key: _decode(item) for key, item in value.items()}
94
+ if isinstance(value, list):
95
+ return [_decode(item) for item in value]
96
+ return value
97
+
98
+
99
+ def _find_class(name: str) -> type | None:
100
+ """Find a class that has not registered yet: import tscp's module, or load plugins."""
101
+ module = name.rpartition(".")[0]
102
+ if module == "tscp" or module.startswith("tscp."):
103
+ try:
104
+ importlib.import_module(module)
105
+ except ImportError:
106
+ return None
107
+ else:
108
+ from tscp import registry # here, not at the top: the registry imports this module
109
+
110
+ registry.load_plugins()
111
+ return _CLASSES.get(name)
112
+
113
+
114
+ def from_config(config: Mapping[str, Any]) -> ConfigMixin:
115
+ """Rebuild an object from :meth:`ConfigMixin.to_config` output.
116
+
117
+ tscp's own modules are imported as needed, and installed plugins (see
118
+ :mod:`tscp.registry`) are loaded when a class is not found otherwise. Only
119
+ :class:`ConfigMixin` subclasses are built, so a config cannot run arbitrary code.
120
+
121
+ Raises:
122
+ ValueError: If no imported module, tscp module or installed plugin defines the class.
123
+ """
124
+ if set(config) != {"class", "args"}:
125
+ raise ValueError("a config must have exactly the keys 'class' and 'args'")
126
+ name = config["class"]
127
+ cls = _CLASSES.get(name) or _find_class(name)
128
+ if cls is None:
129
+ raise ValueError(
130
+ f"unknown class {name!r}; import the module that defines it, or install its plugin"
131
+ )
132
+ return cls(**{key: _decode(value) for key, value in config["args"].items()})
133
+
134
+
135
+ __all__ = ["ConfigMixin", "from_config", "qualified_name"]
tscp/utils/hashing.py ADDED
@@ -0,0 +1,35 @@
1
+ """Fingerprints of arrays and configs, for file checks and artifact IDs."""
2
+
3
+ import hashlib
4
+ import json
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ import torch
10
+
11
+
12
+ def _as_numpy(value: np.ndarray | torch.Tensor) -> np.ndarray:
13
+ if isinstance(value, torch.Tensor):
14
+ value = value.detach().cpu().numpy()
15
+ return np.ascontiguousarray(value)
16
+
17
+
18
+ def array_fingerprint(arrays: Mapping[str, np.ndarray | torch.Tensor]) -> str:
19
+ """SHA-256 over array names, dtypes, shapes and bytes, in sorted name order."""
20
+ digest = hashlib.sha256()
21
+ for name in sorted(arrays):
22
+ array = _as_numpy(arrays[name])
23
+ header = json.dumps([name, array.dtype.str, list(array.shape)])
24
+ digest.update(header.encode())
25
+ digest.update(array.tobytes())
26
+ return digest.hexdigest()
27
+
28
+
29
+ def config_hash(config: Mapping[str, Any]) -> str:
30
+ """SHA-256 of a config's canonical JSON (sorted keys, no NaN)."""
31
+ text = json.dumps(config, sort_keys=True, separators=(",", ":"), allow_nan=False)
32
+ return hashlib.sha256(text.encode()).hexdigest()
33
+
34
+
35
+ __all__ = ["array_fingerprint", "config_hash"]
@@ -0,0 +1,7 @@
1
+ """Axis 3, calibration weights: which past errors count, and how much."""
2
+
3
+ from .base import Weights
4
+ from .exponential import ExponentialWeights
5
+ from .uniform import UniformWeights
6
+
7
+ __all__ = ["ExponentialWeights", "UniformWeights", "Weights"]
tscp/weights/base.py ADDED
@@ -0,0 +1,22 @@
1
+ """The calibration-weights interface (axis 3)."""
2
+
3
+ from torch import Tensor
4
+
5
+ from tscp.data import ResidualArchive
6
+ from tscp.utils import Component
7
+
8
+
9
+ class Weights(Component):
10
+ """Which past errors count, and how much.
11
+
12
+ :meth:`forward` returns nonnegative, unnormalized weights of shape ``[Q, P]``, or
13
+ ``[Q, P, *kept]`` when they differ by step or series. The composer multiplies them by
14
+ the availability and observed masks, so a weight class never handles either.
15
+ """
16
+
17
+ def forward(self, pool: ResidualArchive, query: ResidualArchive) -> Tensor:
18
+ """Return weights of the ``P`` pool rows for each of the ``Q`` query rows."""
19
+ raise NotImplementedError
20
+
21
+
22
+ __all__ = ["Weights"]
@@ -0,0 +1,37 @@
1
+ """Weights that decay with age (NexCP)."""
2
+
3
+ import torch
4
+ from torch import Tensor
5
+
6
+ from tscp.data import ResidualArchive
7
+ from tscp.registry import register
8
+ from tscp.weights.base import Weights
9
+
10
+
11
+ @register("weights", "exponential")
12
+ class ExponentialWeights(Weights):
13
+ """``decay ** (query_origin - pool_origin)``: recent errors count more.
14
+
15
+ With contiguous origins and an expanding pool, the most recent residual gets
16
+ ``decay``, the one before ``decay**2``, and so on: the fixed weights of NexCP
17
+ (Barber, Candès, Ramdas & Tibshirani, Annals of Statistics 2023). Age counts base time
18
+ steps, so an archive whose origins are ``k`` steps apart decays by ``decay**k`` per row.
19
+
20
+ Args:
21
+ decay: Weight ratio per base time step, in (0, 1]; 1 gives equal weights.
22
+ """
23
+
24
+ def __init__(self, decay: float = 0.99):
25
+ super().__init__()
26
+ if not 0 < decay <= 1:
27
+ raise ValueError("decay must lie in (0, 1]")
28
+ self.decay = float(decay)
29
+
30
+ def forward(self, pool: ResidualArchive, query: ResidualArchive) -> Tensor:
31
+ # Rows after the query get age 0 here; the availability mask removes them anyway.
32
+ age = (query.origins[:, None] - pool.origins[None, :]).clamp(min=0)
33
+ decay = torch.tensor(self.decay, dtype=torch.float64, device=pool.device)
34
+ return decay.pow(age.to(torch.float64))
35
+
36
+
37
+ __all__ = ["ExponentialWeights"]
@@ -0,0 +1,23 @@
1
+ """Equal weights."""
2
+
3
+ import torch
4
+ from torch import Tensor
5
+
6
+ from tscp.data import ResidualArchive
7
+ from tscp.registry import register
8
+ from tscp.weights.base import Weights
9
+
10
+
11
+ @register("weights", "uniform")
12
+ class UniformWeights(Weights):
13
+ """Every pool residual counts the same.
14
+
15
+ Passing ``weights=None`` to a predictor gives the same thresholds through a faster
16
+ path; this class exists for ablations that swap weight classes.
17
+ """
18
+
19
+ def forward(self, pool: ResidualArchive, query: ResidualArchive) -> Tensor:
20
+ return torch.ones(len(query), len(pool), dtype=torch.float64, device=pool.device)
21
+
22
+
23
+ __all__ = ["UniformWeights"]
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.5
2
+ Name: tscp
3
+ Version: 0.1.0.dev0
4
+ Summary: Conformal prediction for time series, in PyTorch
5
+ Project-URL: Documentation, https://tscp.readthedocs.io
6
+ Project-URL: Source, https://github.com/RobertoNeglia/tscp
7
+ Project-URL: Issues, https://github.com/RobertoNeglia/tscp/issues
8
+ Project-URL: Changelog, https://github.com/RobertoNeglia/tscp/blob/main/CHANGELOG.md
9
+ Author: Roberto Neglia
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: conformal-prediction,forecasting,prediction-intervals,pytorch,time-series,uncertainty-quantification
13
+ Classifier: Development Status :: 2 - Pre-Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: numpy>=1.26
23
+ Requires-Dist: torch>=2.2
24
+ Provides-Extra: dev
25
+ Requires-Dist: furo; extra == 'dev'
26
+ Requires-Dist: myst-parser>=3; extra == 'dev'
27
+ Requires-Dist: pre-commit; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
29
+ Requires-Dist: pytest>=8; extra == 'dev'
30
+ Requires-Dist: pyyaml>=6; extra == 'dev'
31
+ Requires-Dist: ruff==0.17.4; extra == 'dev'
32
+ Requires-Dist: sphinx-copybutton; extra == 'dev'
33
+ Requires-Dist: sphinx<9,>=7; extra == 'dev'
34
+ Provides-Extra: doc
35
+ Requires-Dist: furo; extra == 'doc'
36
+ Requires-Dist: myst-parser>=3; extra == 'doc'
37
+ Requires-Dist: sphinx-copybutton; extra == 'doc'
38
+ Requires-Dist: sphinx<9,>=7; extra == 'doc'
39
+ Provides-Extra: examples
40
+ Requires-Dist: pyyaml>=6; extra == 'examples'
41
+ Provides-Extra: test
42
+ Requires-Dist: pytest-cov>=5; extra == 'test'
43
+ Requires-Dist: pytest>=8; extra == 'test'
44
+ Requires-Dist: pyyaml>=6; extra == 'test'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # tscp
48
+
49
+ [![CI](https://github.com/RobertoNeglia/tscp/actions/workflows/ci.yaml/badge.svg)](https://github.com/RobertoNeglia/tscp/actions/workflows/ci.yaml)
50
+ [![Documentation](https://readthedocs.org/projects/tscp/badge/?version=latest)](https://tscp.readthedocs.io/en/latest/)
51
+
52
+ Conformal prediction for time series, in PyTorch: the published methods behind one interface,
53
+ from one-step intervals to joint regions over a forecast horizon and across series, with one
54
+ evaluation protocol.
55
+
56
+ **Status:** early development. The data contract, the component interfaces, contract
57
+ checks for new components, a registry with plugins, feature maps, helpers for learned
58
+ components and saved methods are in place, with split CP, NexCP and ACI and the absolute,
59
+ signed, normalized and CQR scores; more methods will follow.
60
+
61
+ ## Install
62
+
63
+ tscp is in early development; its pre-releases install with
64
+
65
+ ```bash
66
+ pip install --pre tscp
67
+ ```
68
+
69
+ For development, clone the repository and install it with `pip install -e ".[dev]"`.
70
+ Documentation: https://tscp.readthedocs.io
71
+
72
+ ## Quickstart
73
+
74
+ ```python
75
+ from tscp.datasets import variance_shift
76
+ from tscp.methods import get_method
77
+ from tscp.metrics import evaluate
78
+
79
+ archive = variance_shift(horizon=1) # a synthetic residual archive
80
+ method = get_method("nexcp", decay=0.99, alpha=0.1)
81
+ region = method.fit(archive).calibrate(archive).predict(archive)
82
+ print(evaluate(region, archive.split("test"), alpha=0.1, window=200))
83
+ ```
84
+
85
+ `examples/quickstart.py` compares the three methods on two synthetic archives.
86
+
87
+ ## Build your own method
88
+
89
+ A method is one component per axis: residual source, score, calibration weights, quantile
90
+ model, online update. `compose` builds one from short names, such as a spec read from YAML:
91
+
92
+ ```python
93
+ from tscp.methods import compose
94
+
95
+ method = compose(
96
+ {
97
+ "score": "signed",
98
+ "source": "expanding",
99
+ "weights": {"name": "exponential", "decay": 0.99},
100
+ "update": {"name": "aci", "gamma": 0.005},
101
+ }
102
+ )
103
+ ```
104
+
105
+ A new component subclasses its axis's base class, and `tscp.testing` checks that it keeps
106
+ the contracts every method relies on, such as causality and split discipline:
107
+
108
+ ```python
109
+ from tscp.testing import check_method
110
+
111
+ check_method(lambda: compose({"score": "signed", "update": "aci"}))
112
+ ```
113
+
114
+ Packages can register components under short names as plugins. The tutorials in `docs/`
115
+ cover composing methods, writing components, learned components, plugins and batch
116
+ production; the code they show is in `examples/` and runs in the tests.
117
+
118
+ ## Batch production
119
+
120
+ ```python
121
+ from tscp.data import ResidualArchive
122
+ from tscp.methods import load_method, save_method
123
+
124
+ archive = ResidualArchive.from_forecasts(
125
+ truth, forecasts, sizes={"fit": 0.2, "calibration": 0.3, "test": None}
126
+ )
127
+ save_method(method.fit(archive), "method.npz") # one file, no pickle
128
+ region = load_method("method.npz").calibrate(archive).predict(archive)
129
+ intervals = region.to_target(archive.split("test").forecasts)
130
+ ```
@@ -0,0 +1,72 @@
1
+ tscp/__init__.py,sha256=XyXcv9ftZ7p3pNbCm07ICK1IsYfeCAckjVMod2xg4Pc,906
2
+ tscp/imports.py,sha256=5LlxLGqOIq0xGL0WYiN0qwpessZpOJ-Ow34vbxDNM14,1064
3
+ tscp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ tscp/registry.py,sha256=G0SA8iHR0Ql8S8NQSm4-fp830aTOhwBi2nWAryfjO64,5923
5
+ tscp/typing.py,sha256=z8ks7eXoGialkTKwMVUmHGwTQLL7Gs2Gigj5mF_8BFA,639
6
+ tscp/data/__init__.py,sha256=HVOThntO8YCU-o_Aqea3OYstMF55yO6IHlv0eqyB198,526
7
+ tscp/data/archive.py,sha256=NlAX0S5_drV3WlYBGOCYJXNCnJK6mZZOIFOXcPQgHgY,22439
8
+ tscp/data/availability.py,sha256=lxS3BjIjZxyNBZ-KpuYhreAGvkJsA3aMEDN1ufhqHt0,2394
9
+ tscp/data/io.py,sha256=OmGcXd4TlrFQZrs57urq2NP2Svdv1aBE8czN3fqm_w0,4531
10
+ tscp/data/splits.py,sha256=V42Vz4dDKkQVKHB4eDV931X6aiRVB-OUB3DVM7jIVkE,7243
11
+ tscp/datasets/__init__.py,sha256=Lv000yDMT30NNYabLMZmPfmeHAQph_flm5BpgA8rxh0,213
12
+ tscp/datasets/synthetic.py,sha256=zRosExAOBsIP0W5oSE1-j1Znt-Y91O3d6mrk4xJ7DnE,6349
13
+ tscp/features/__init__.py,sha256=rDOYjFANK8GkwwfB8ijjwsJ3eb4E6a2KglUCVoJkNtk,531
14
+ tscp/features/base.py,sha256=Zqk7dQv9vQihH2guIt-XNm7PObX6kz5h67jmr-FGOp0,2582
15
+ tscp/features/calendar.py,sha256=ChxOv4Eisa5wFYD1BK9DZ4vPD8vwIV0tqX2KoyV4S18,4477
16
+ tscp/features/combine.py,sha256=lluRzcCIyuhzcTVz3NiXiZzzhf4qssrAVbz1bLUYCjo,2476
17
+ tscp/features/context.py,sha256=6pWMkM4gfx420od7GWmumc-Gja1E4CjsnhImt_XdL6Y,1528
18
+ tscp/features/history.py,sha256=c8xB0KWDfHz42V0WIZDUlxcviROQof0jLB419p4Jol8,2364
19
+ tscp/features/phase.py,sha256=4Dh73pz-PtTU0AdGkUtSpCHq5OLVcCvlBZfX7elBY0w,1563
20
+ tscp/methods/__init__.py,sha256=uVb3FDrpNxPeidQflbG0n7-iwWgTc2H4_g8IPicnuYQ,1557
21
+ tscp/methods/aci.py,sha256=0frJVOgcNjzDueoGfkKo1vqnFZRVt6hhsaiUT31jLqE,1564
22
+ tscp/methods/base.py,sha256=SJmBipb9ewoCxbQ1hoGeQGpJLZ9SKJ4kBeJ8gmGFr90,10599
23
+ tscp/methods/compose.py,sha256=305tJ5QstMDqhhfIQS8jytHYrlW3C6Vm409vVEyqajw,3385
24
+ tscp/methods/engine.py,sha256=CT1Dv8lzz-kz6N3UmToTN8KK2fj4Xfg5WK272dr5SWs,5082
25
+ tscp/methods/io.py,sha256=5HXcLf4c0W0MH2s_drGf1ZEBzjOvYiWYIcKK9BQ4wCM,4564
26
+ tscp/methods/nexcp.py,sha256=7_4YFxvBw6d2BskyEOKyzHnYjBOAbh3U55XvVmddxZw,1521
27
+ tscp/methods/split.py,sha256=k624L9mfRS922EsCTeYwq-hP-iFjYKsnVO5PbSaZ2ec,982
28
+ tscp/metrics/__init__.py,sha256=wkeSKqbWINceHTalFRtQazRXL18Nd3K6_Z6ODK5J_Wc,701
29
+ tscp/metrics/conditional.py,sha256=5DRPwHeKjgVfZl6UwBwyDe5XG5PbyTVQKpuvj_1q5IQ,2693
30
+ tscp/metrics/interval.py,sha256=gYWuJDQnTCOgp_VLWTpq9s8eYHrbpXm2xvP3Q_-ET0s,2522
31
+ tscp/metrics/report.py,sha256=ibw2_TjA-TjpKfhQivmxqmEPuIKIYmIWmvXYvNMs5O8,5711
32
+ tscp/nn/__init__.py,sha256=FOzUoA7c5jT5fHFBJvI0aJBcdEMB4wwZyOCfzcFJ-AQ,650
33
+ tscp/nn/data.py,sha256=eoHypy5sXhiVAb8TOYF_QlHITxzNOtmM0xMsR-8OWNw,3072
34
+ tscp/nn/losses.py,sha256=Hgssr1yGa8XHLvrCexCzGVUKh22s7qbhvtB3nfOzuyk,2200
35
+ tscp/nn/training.py,sha256=1J6RKxzeYRk90ZZHPuJBs-9WjmaiWfiEs5Cog3S4AGA,4953
36
+ tscp/online/__init__.py,sha256=fVXtkisUIYuBxqL6n-WGnELZ6re1KN-xGBYl6FpEKYM,173
37
+ tscp/online/aci.py,sha256=gff6JBWUPi5wXG-2Yw4HBc0prAYualgpF5nO5BLM88Q,1850
38
+ tscp/online/base.py,sha256=8UfGOuzzDNUdGLrUDc-3o7K-ve_Uq-hPrcvtBDx88p4,2647
39
+ tscp/quantile/__init__.py,sha256=1IspG7zwuo1tvI2eubyRSynC6-UB9LI6xsczVRrnyfM,455
40
+ tscp/quantile/base.py,sha256=2D0sp93uRLmDrtYG_nbXXyG7l4EuM4BQSJ11DeUbv-U,2815
41
+ tscp/quantile/distribution.py,sha256=sJFU0x7vRRNXI1yRg0Hp91TyHGf8k0mdR653Lc7uqRs,8367
42
+ tscp/quantile/weighted.py,sha256=1_ldRSpJCHriodqVuqH9eaKmyqmCtr4frm1QUbKN38M,4404
43
+ tscp/regions/__init__.py,sha256=7zQ9W4IjjLQ_wZUQZy1CyC9OUsnXRDqrUBkXJBYIG9c,375
44
+ tscp/regions/base.py,sha256=9hRKmvPmZiGdUd8fRCb2oZPJHptDESBoRNENBXhKT6w,5852
45
+ tscp/regions/box.py,sha256=8yvP5qp7GKCRux3Oov_UU59z8NfhPqcijX-G3JCEMVo,3506
46
+ tscp/regions/interval.py,sha256=glRjtY_Q0quNC-HtDkWmXL3-7J00aLNctZPyciiQoOc,1943
47
+ tscp/scores/__init__.py,sha256=WcVz-kwxmoQSTNLI4upUNzr7OL-oBlqrquvnDOA0cWo,295
48
+ tscp/scores/absolute.py,sha256=fk9SS5-Dsm_32HOBtJ3TS_oZnF94oMZUxd-jBwS_CEo,598
49
+ tscp/scores/base.py,sha256=lBdpgi-mBojLEeseYETymiZ9St4dbPkjUYjEVDj_HHo,4058
50
+ tscp/scores/cqr.py,sha256=SOmviVYXkF0m15N6lbJimgEwOCXa6eZwx8DNjs8bOM8,3100
51
+ tscp/scores/normalized.py,sha256=bReBPmz-pGmMbuf3ZkcdfoQ_WVP3yrPWgk3VNQPQJRA,2907
52
+ tscp/scores/signed.py,sha256=Cco8jSA4p3cNWmz_T0BjM9SCd5QkkOZfRTKOD1zrHus,1678
53
+ tscp/sources/__init__.py,sha256=Ku2E2GLeCQhnh677gz9mjJ9fmwIec27a4EANU5Udm2Q,256
54
+ tscp/sources/base.py,sha256=laZ6GSgFD8dKcWj06wUdUBqrC6XTIaDadXHNg7jcwd0,1101
55
+ tscp/sources/expanding.py,sha256=G7QDukpHZLtrtfsZogeW7rrjjgA6lXOzME_1X9jr-5o,759
56
+ tscp/sources/holdout.py,sha256=6Ud_TG_D32y8MWj832srGRwou8zpAkqMc3Qc55fpS9Y,474
57
+ tscp/testing/__init__.py,sha256=39-Vjf8hQA-hQ51z22SOxmQjW5r5S4Y-xCVENK8wOrg,1070
58
+ tscp/testing/archives.py,sha256=M7CFYQwWfK0JxKNCWI3V6_gvixPwEsfqPCD_RbZEIfw,2963
59
+ tscp/testing/checks.py,sha256=-SOrjnUH5FbW0LpW_zb_OvjANNe4fThP65QQY1FwVew,34227
60
+ tscp/testing/components.py,sha256=31sCJy0Tt9vR6fKUZ-ntfO5jJsOE9f57X8VBu1N6CAQ,917
61
+ tscp/utils/__init__.py,sha256=MYndMPUN70j9WWCkNWsjNJYvjyALj1rIdYeKsgO2bNo,357
62
+ tscp/utils/component.py,sha256=TYQlrumoYmQGu9oOxMzPvFMLt5yAgCxFXkBh05tSzHE,3186
63
+ tscp/utils/config.py,sha256=RkxqNWZZ08rbOSnvrKh5hfx-gK7AWJC6vwCF6stYT8Q,5444
64
+ tscp/utils/hashing.py,sha256=DNoaHizzqnIlMiD5XprYcquBN0yCCUPP2WjN4QUBhF0,1143
65
+ tscp/weights/__init__.py,sha256=hnIQLCLsYMwXB09led7UTfLDin253VW8XzT8SBu8y2M,244
66
+ tscp/weights/base.py,sha256=XTWc9-Z5JP7PhU9IND1c8molYo-K4pO6JM6OGFYCJ4Q,713
67
+ tscp/weights/exponential.py,sha256=8heV7qvCsOtIV_CueSIgixu3EklOUielJua3wJCLb9g,1400
68
+ tscp/weights/uniform.py,sha256=peFO2WQTe8NyscVUjdjJjz3uELi7eKqYA8ZurYcZKdA,647
69
+ tscp-0.1.0.dev0.dist-info/METADATA,sha256=EaGb08mubAR8XhL8wPW2EcRVZXpvjzlWG18S-YntD18,4889
70
+ tscp-0.1.0.dev0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
71
+ tscp-0.1.0.dev0.dist-info/licenses/LICENSE,sha256=eBmYQftEbuPRgifQ78YlvwS6ypE0zMfZoUdjBbUCAPk,1071
72
+ tscp-0.1.0.dev0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roberto Neglia
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.