views-frames 1.0.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.
- views_frames/__init__.py +41 -0
- views_frames/_typing.py +24 -0
- views_frames/_validation.py +114 -0
- views_frames/conformance/__init__.py +123 -0
- views_frames/feature_frame.py +188 -0
- views_frames/index.py +309 -0
- views_frames/io/__init__.py +13 -0
- views_frames/io/arrow.py +103 -0
- views_frames/io/npz.py +59 -0
- views_frames/metadata.py +36 -0
- views_frames/prediction_frame.py +143 -0
- views_frames/protocols.py +82 -0
- views_frames/py.typed +0 -0
- views_frames/spatial_level.py +41 -0
- views_frames/target_frame.py +138 -0
- views_frames-1.0.0.dist-info/METADATA +624 -0
- views_frames-1.0.0.dist-info/RECORD +27 -0
- views_frames-1.0.0.dist-info/WHEEL +4 -0
- views_frames-1.0.0.dist-info/licenses/LICENSE +21 -0
- views_frames_summarize/__init__.py +29 -0
- views_frames_summarize/_common.py +68 -0
- views_frames_summarize/aggregate.py +83 -0
- views_frames_summarize/collapse.py +37 -0
- views_frames_summarize/conformance.py +40 -0
- views_frames_summarize/interval.py +62 -0
- views_frames_summarize/point.py +104 -0
- views_frames_summarize/py.typed +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Published protocols — the abstract surface consumers type against.
|
|
2
|
+
|
|
3
|
+
Consumers depend on these `Protocol`s (DIP/ISP, ADR-009, README §5); a concrete
|
|
4
|
+
frame is an implementation detail. The surface is segregated so no consumer
|
|
5
|
+
depends on methods it does not use.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from numpy.typing import NDArray
|
|
15
|
+
|
|
16
|
+
from views_frames._typing import IntArray
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from views_frames.index import SpatioTemporalIndex
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class SpatioTemporalIndexed(Protocol):
|
|
24
|
+
"""What a reconciler / aligner needs: the row identity surface."""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def n_rows(self) -> int:
|
|
28
|
+
"""Number of rows (the first axis length)."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def identifiers(self) -> dict[str, IntArray]:
|
|
33
|
+
"""The integer identifier arrays, keyed by name (e.g. ``time``, ``unit``)."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def index(self) -> SpatioTemporalIndex:
|
|
38
|
+
"""The row index — the handle for alignment (``cross_level_align`` etc.)."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@runtime_checkable
|
|
43
|
+
class Sampled(Protocol):
|
|
44
|
+
"""Structural facts about the trailing sample axis (ADR-012).
|
|
45
|
+
|
|
46
|
+
Reduction over the sample axis (mean/MAP/HDI/quantiles) is **not** here — it
|
|
47
|
+
lives in the ``views_frames_summarize`` sibling package (ADR-017).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def sample_count(self) -> int:
|
|
52
|
+
"""Size of the trailing sample axis ``S`` (always ``>= 1``)."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_sample(self) -> bool:
|
|
57
|
+
"""True iff ``sample_count > 1``."""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@runtime_checkable
|
|
62
|
+
class Persistable(Protocol):
|
|
63
|
+
"""What I/O needs — and only I/O."""
|
|
64
|
+
|
|
65
|
+
def save(self, directory: Path | str) -> None:
|
|
66
|
+
"""Serialize this frame to ``directory``."""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def load(cls, directory: Path | str, mmap: bool = False) -> Persistable:
|
|
71
|
+
"""Deserialize a frame from ``directory``; ``mmap`` propagates (C-07)."""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@runtime_checkable
|
|
76
|
+
class Frame(SpatioTemporalIndexed, Protocol):
|
|
77
|
+
"""The small composition the math layer needs: values + index + n_rows."""
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def values(self) -> NDArray[np.float32]:
|
|
81
|
+
"""The contiguous ``float32`` value array (first axis = rows)."""
|
|
82
|
+
...
|
views_frames/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""`SpatialLevel` — the cm/pgm identifier vocabulary (ADR-015).
|
|
2
|
+
|
|
3
|
+
Labels only: `entity_column` + **time-first** `index_names`. This object never
|
|
4
|
+
carries the cross-level mapping (ADR-014), unit values, ranges, or the grid
|
|
5
|
+
backbone. stdlib only (no numpy, no domain data).
|
|
6
|
+
|
|
7
|
+
Relocated from `views-pipeline-core/.../domain/spatial.py` with the two known
|
|
8
|
+
defects *fixed, not ported* (ADR-015, register C-18): the index tuple is
|
|
9
|
+
time-first ``(month_id, entity)`` (was entity-first, C-65), and the priogrid
|
|
10
|
+
entity name is consistent (`priogrid_id` everywhere).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from enum import Enum
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SpatialLevel(Enum):
|
|
19
|
+
"""Spatial level of a frame's rows: country-month or PRIO-GRID-month.
|
|
20
|
+
|
|
21
|
+
Carries the level's identifier vocabulary and nothing else.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
CM = "cm"
|
|
25
|
+
PGM = "pgm"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def entity_column(self) -> str:
|
|
29
|
+
"""The unit identifier column name for this level."""
|
|
30
|
+
return _ENTITY_COLUMN[self]
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def index_names(self) -> tuple[str, str]:
|
|
34
|
+
"""The ``(time, entity)`` index column names — **time-first** (ADR-015)."""
|
|
35
|
+
return ("month_id", self.entity_column)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_ENTITY_COLUMN: dict[SpatialLevel, str] = {
|
|
39
|
+
SpatialLevel.CM: "country_id",
|
|
40
|
+
SpatialLevel.PGM: "priogrid_id",
|
|
41
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""`TargetFrame` — observed actuals (ground truth): ``y_true (N, 1)`` float32.
|
|
2
|
+
|
|
3
|
+
A sibling frame (no shared base; ADR-011 Option C). Structurally a
|
|
4
|
+
`PredictionFrame` with ``S == 1`` (the trailing sample axis is explicit; ADR-012).
|
|
5
|
+
Makes the evaluation boundary array-native. ``is_sample`` is always ``False``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from numpy.typing import NDArray
|
|
14
|
+
|
|
15
|
+
from views_frames._typing import IntArray
|
|
16
|
+
from views_frames._validation import coerce_values, validate_values
|
|
17
|
+
from views_frames.index import SpatioTemporalIndex
|
|
18
|
+
from views_frames.io import npz
|
|
19
|
+
from views_frames.metadata import FrameMetadata
|
|
20
|
+
from views_frames.spatial_level import SpatialLevel
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TargetFrame:
|
|
24
|
+
"""Immutable observed-actuals frame: ``(N, 1)`` float32 + a spatiotemporal index."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
y_true: object,
|
|
29
|
+
index: SpatioTemporalIndex,
|
|
30
|
+
metadata: FrameMetadata | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
values = coerce_values(y_true)
|
|
33
|
+
validate_values(values)
|
|
34
|
+
if values.ndim != 2 or values.shape[1] != 1:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"TargetFrame y_true must have shape (N, 1) with an explicit "
|
|
37
|
+
f"trailing axis (ADR-012), got shape {values.shape}"
|
|
38
|
+
)
|
|
39
|
+
if values.shape[0] != index.n_rows:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"y_true has {values.shape[0]} rows but index has {index.n_rows}"
|
|
42
|
+
)
|
|
43
|
+
self._values = values
|
|
44
|
+
self._index = index
|
|
45
|
+
self._metadata = metadata if metadata is not None else FrameMetadata()
|
|
46
|
+
|
|
47
|
+
# ---- core surface -------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def values(self) -> NDArray[np.float32]:
|
|
51
|
+
"""The ``(N, 1)`` float32 value array."""
|
|
52
|
+
return self._values
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def index(self) -> SpatioTemporalIndex:
|
|
56
|
+
"""The spatiotemporal row index."""
|
|
57
|
+
return self._index
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def metadata(self) -> FrameMetadata:
|
|
61
|
+
"""The typed provenance header."""
|
|
62
|
+
return self._metadata
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def n_rows(self) -> int:
|
|
66
|
+
"""Number of rows ``N``."""
|
|
67
|
+
return int(self._values.shape[0])
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def identifiers(self) -> dict[str, IntArray]:
|
|
71
|
+
"""The integer identifier arrays from the index."""
|
|
72
|
+
return self._index.identifiers
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def sample_count(self) -> int:
|
|
76
|
+
"""Always ``1`` for a target frame."""
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def is_sample(self) -> bool:
|
|
81
|
+
"""Always ``False`` — a target carries a single realized value."""
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def with_metadata(self, metadata: FrameMetadata) -> TargetFrame:
|
|
85
|
+
"""Return a new frame with replaced metadata, **sharing** the values buffer."""
|
|
86
|
+
new = TargetFrame.__new__(TargetFrame)
|
|
87
|
+
new._values = self._values
|
|
88
|
+
new._index = self._index
|
|
89
|
+
new._metadata = metadata
|
|
90
|
+
return new
|
|
91
|
+
|
|
92
|
+
def select(self, indexer: IntArray | NDArray[np.bool_]) -> TargetFrame:
|
|
93
|
+
"""A new frame of the rows at integer positions **or** a boolean mask.
|
|
94
|
+
|
|
95
|
+
Rows are selected by numpy fancy indexing — an integer array reorders or
|
|
96
|
+
repeats, a boolean mask filters. Metadata is preserved; the selection
|
|
97
|
+
**copies**. An empty selection yields an empty frame.
|
|
98
|
+
"""
|
|
99
|
+
return TargetFrame(
|
|
100
|
+
self._values[indexer], self._index.select(indexer), self._metadata
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def reindex(self, other: SpatioTemporalIndex) -> TargetFrame:
|
|
104
|
+
"""Align this frame to ``other``'s rows, returning a new frame.
|
|
105
|
+
|
|
106
|
+
Fails loud unless this frame's index is a **superset** of ``other``. The
|
|
107
|
+
frame-level companion to the index's ``reindex``/``searchsorted``.
|
|
108
|
+
"""
|
|
109
|
+
if not self._index.is_superset_of(other):
|
|
110
|
+
raise ValueError(
|
|
111
|
+
"reindex requires this frame's index to be a superset of `other`; "
|
|
112
|
+
"some target rows are absent"
|
|
113
|
+
)
|
|
114
|
+
return self.select(self._index.searchsorted(other))
|
|
115
|
+
|
|
116
|
+
# ---- persistence --------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def save(self, directory: Path | str) -> None:
|
|
119
|
+
"""Serialize to ``directory`` (npy + npz + header)."""
|
|
120
|
+
npz.save(
|
|
121
|
+
directory,
|
|
122
|
+
values=self._values,
|
|
123
|
+
time=self._index.time,
|
|
124
|
+
unit=self._index.unit,
|
|
125
|
+
level=self._index.level.value,
|
|
126
|
+
metadata=self._metadata.to_dict(),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def load(cls, directory: Path | str, mmap: bool = False) -> TargetFrame:
|
|
131
|
+
"""Deserialize a frame from ``directory``; ``mmap`` propagates."""
|
|
132
|
+
state = npz.load(directory, mmap=mmap)
|
|
133
|
+
index = SpatioTemporalIndex(
|
|
134
|
+
time=state["time"],
|
|
135
|
+
unit=state["unit"],
|
|
136
|
+
level=SpatialLevel(state["level"]),
|
|
137
|
+
)
|
|
138
|
+
return cls(state["values"], index, FrameMetadata.from_dict(state["metadata"]))
|