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.
- tscp/__init__.py +45 -0
- tscp/data/__init__.py +19 -0
- tscp/data/archive.py +488 -0
- tscp/data/availability.py +55 -0
- tscp/data/io.py +109 -0
- tscp/data/splits.py +169 -0
- tscp/datasets/__init__.py +6 -0
- tscp/datasets/synthetic.py +177 -0
- tscp/features/__init__.py +22 -0
- tscp/features/base.py +64 -0
- tscp/features/calendar.py +105 -0
- tscp/features/combine.py +69 -0
- tscp/features/context.py +42 -0
- tscp/features/history.py +58 -0
- tscp/features/phase.py +41 -0
- tscp/imports.py +32 -0
- tscp/methods/__init__.py +48 -0
- tscp/methods/aci.py +44 -0
- tscp/methods/base.py +228 -0
- tscp/methods/compose.py +90 -0
- tscp/methods/engine.py +128 -0
- tscp/methods/io.py +106 -0
- tscp/methods/nexcp.py +42 -0
- tscp/methods/split.py +30 -0
- tscp/metrics/__init__.py +30 -0
- tscp/metrics/conditional.py +66 -0
- tscp/metrics/interval.py +75 -0
- tscp/metrics/report.py +135 -0
- tscp/nn/__init__.py +13 -0
- tscp/nn/data.py +85 -0
- tscp/nn/losses.py +63 -0
- tscp/nn/training.py +125 -0
- tscp/online/__init__.py +6 -0
- tscp/online/aci.py +53 -0
- tscp/online/base.py +67 -0
- tscp/py.typed +0 -0
- tscp/quantile/__init__.py +16 -0
- tscp/quantile/base.py +73 -0
- tscp/quantile/distribution.py +170 -0
- tscp/quantile/weighted.py +103 -0
- tscp/regions/__init__.py +15 -0
- tscp/regions/base.py +139 -0
- tscp/regions/box.py +82 -0
- tscp/regions/interval.py +62 -0
- tscp/registry.py +179 -0
- tscp/scores/__init__.py +9 -0
- tscp/scores/absolute.py +22 -0
- tscp/scores/base.py +87 -0
- tscp/scores/cqr.py +71 -0
- tscp/scores/normalized.py +74 -0
- tscp/scores/signed.py +46 -0
- tscp/sources/__init__.py +7 -0
- tscp/sources/base.py +32 -0
- tscp/sources/expanding.py +25 -0
- tscp/sources/holdout.py +18 -0
- tscp/testing/__init__.py +40 -0
- tscp/testing/archives.py +80 -0
- tscp/testing/checks.py +832 -0
- tscp/testing/components.py +28 -0
- tscp/typing.py +24 -0
- tscp/utils/__init__.py +14 -0
- tscp/utils/component.py +76 -0
- tscp/utils/config.py +135 -0
- tscp/utils/hashing.py +35 -0
- tscp/weights/__init__.py +7 -0
- tscp/weights/base.py +22 -0
- tscp/weights/exponential.py +37 -0
- tscp/weights/uniform.py +23 -0
- tscp-0.1.0.dev0.dist-info/METADATA +130 -0
- tscp-0.1.0.dev0.dist-info/RECORD +72 -0
- tscp-0.1.0.dev0.dist-info/WHEEL +4 -0
- 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
|
+
]
|
tscp/utils/component.py
ADDED
|
@@ -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"]
|
tscp/weights/__init__.py
ADDED
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"]
|
tscp/weights/uniform.py
ADDED
|
@@ -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
|
+
[](https://github.com/RobertoNeglia/tscp/actions/workflows/ci.yaml)
|
|
50
|
+
[](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,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.
|