tidy3a 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tidy3a/__init__.py +114 -0
- tidy3a/__version__.py +1 -0
- tidy3a/align.py +136 -0
- tidy3a/errors.py +45 -0
- tidy3a/expr.py +911 -0
- tidy3a/formula.py +920 -0
- tidy3a/frame.py +243 -0
- tidy3a/io.py +598 -0
- tidy3a/jupyter.py +314 -0
- tidy3a/linalg.py +192 -0
- tidy3a/py.typed +0 -0
- tidy3a/selectors.py +141 -0
- tidy3a/verbs.py +782 -0
- tidy3a-0.1.0.dist-info/METADATA +142 -0
- tidy3a-0.1.0.dist-info/RECORD +18 -0
- tidy3a-0.1.0.dist-info/WHEEL +5 -0
- tidy3a-0.1.0.dist-info/licenses/LICENSE +21 -0
- tidy3a-0.1.0.dist-info/top_level.txt +1 -0
tidy3a/__init__.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""tidy3A: tidy-style verbs for named multidimensional arrays."""
|
|
2
|
+
|
|
3
|
+
from tidy3a.__version__ import __version__
|
|
4
|
+
from tidy3a.errors import (
|
|
5
|
+
AlignError,
|
|
6
|
+
FilterError,
|
|
7
|
+
GroupError,
|
|
8
|
+
MutateError,
|
|
9
|
+
ReadError,
|
|
10
|
+
RefError,
|
|
11
|
+
SelectError,
|
|
12
|
+
SliceError,
|
|
13
|
+
SummariseError,
|
|
14
|
+
TidyError,
|
|
15
|
+
)
|
|
16
|
+
from tidy3a.expr import (
|
|
17
|
+
all,
|
|
18
|
+
any,
|
|
19
|
+
bins,
|
|
20
|
+
col,
|
|
21
|
+
env,
|
|
22
|
+
fields,
|
|
23
|
+
if_else,
|
|
24
|
+
label,
|
|
25
|
+
max,
|
|
26
|
+
mean,
|
|
27
|
+
median,
|
|
28
|
+
min,
|
|
29
|
+
n,
|
|
30
|
+
std,
|
|
31
|
+
sum,
|
|
32
|
+
var,
|
|
33
|
+
variable,
|
|
34
|
+
)
|
|
35
|
+
from tidy3a.formula import FormulaError, formula
|
|
36
|
+
from tidy3a.frame import TidyArray, tidy
|
|
37
|
+
from tidy3a.io import fixed_text, infer, read_array, read_binary
|
|
38
|
+
from tidy3a.linalg import (
|
|
39
|
+
angle,
|
|
40
|
+
arccos,
|
|
41
|
+
arcsin,
|
|
42
|
+
arctan,
|
|
43
|
+
arctan2,
|
|
44
|
+
ceil,
|
|
45
|
+
conj,
|
|
46
|
+
cos,
|
|
47
|
+
cosh,
|
|
48
|
+
dot,
|
|
49
|
+
exp,
|
|
50
|
+
floor,
|
|
51
|
+
imag,
|
|
52
|
+
log,
|
|
53
|
+
log2,
|
|
54
|
+
log10,
|
|
55
|
+
pack,
|
|
56
|
+
real,
|
|
57
|
+
rename_axes,
|
|
58
|
+
sign,
|
|
59
|
+
sin,
|
|
60
|
+
sinh,
|
|
61
|
+
sqrt,
|
|
62
|
+
tan,
|
|
63
|
+
tanh,
|
|
64
|
+
)
|
|
65
|
+
from tidy3a.selectors import (
|
|
66
|
+
all_of,
|
|
67
|
+
any_of,
|
|
68
|
+
contains,
|
|
69
|
+
ends_with,
|
|
70
|
+
everything,
|
|
71
|
+
is_numeric,
|
|
72
|
+
last_col,
|
|
73
|
+
matches,
|
|
74
|
+
num_range,
|
|
75
|
+
starts_with,
|
|
76
|
+
where,
|
|
77
|
+
)
|
|
78
|
+
from tidy3a.verbs import (
|
|
79
|
+
assign,
|
|
80
|
+
bind_cols,
|
|
81
|
+
filter,
|
|
82
|
+
full_join,
|
|
83
|
+
group_by,
|
|
84
|
+
inner_join,
|
|
85
|
+
left_join,
|
|
86
|
+
mutate,
|
|
87
|
+
pull,
|
|
88
|
+
select,
|
|
89
|
+
slice,
|
|
90
|
+
slice_head,
|
|
91
|
+
slice_tail,
|
|
92
|
+
summarise,
|
|
93
|
+
summarize,
|
|
94
|
+
ungroup,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
nan = float("nan")
|
|
98
|
+
|
|
99
|
+
__all__ = [
|
|
100
|
+
"AlignError", "FilterError", "GroupError", "MutateError", "ReadError", "RefError",
|
|
101
|
+
"SelectError", "SliceError", "SummariseError", "TidyArray", "TidyError", "__version__",
|
|
102
|
+
"all", "all_of", "any", "any_of", "assign", "bind_cols", "bins", "col", "contains",
|
|
103
|
+
"ends_with", "env", "everything", "fields", "filter", "fixed_text", "full_join", "group_by", "if_else",
|
|
104
|
+
"infer", "inner_join", "is_numeric", "label", "last_col", "left_join", "matches", "max",
|
|
105
|
+
"mean", "median", "min", "mutate", "n", "nan", "num_range", "pull", "read_array", "read_binary", "select",
|
|
106
|
+
"slice", "slice_head", "slice_tail", "starts_with", "std", "sum", "summarise", "summarize",
|
|
107
|
+
"tidy", "ungroup", "var", "variable", "where",
|
|
108
|
+
# formula
|
|
109
|
+
"FormulaError", "formula",
|
|
110
|
+
# array layer
|
|
111
|
+
"angle", "arccos", "arcsin", "arctan", "arctan2", "ceil", "conj", "cos", "cosh", "dot",
|
|
112
|
+
"exp", "floor", "imag", "log", "log2", "log10", "pack", "real", "rename_axes", "sign",
|
|
113
|
+
"sin", "sinh", "sqrt", "tan", "tanh",
|
|
114
|
+
]
|
tidy3a/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
tidy3a/align.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Alignment rules (plan §5.5).
|
|
2
|
+
|
|
3
|
+
Labels on a shared axis must match exactly. The same labels in another
|
|
4
|
+
order are matched by label and reported; different labels need a join
|
|
5
|
+
verb. A labelled axis never pairs with an unlabelled one by length.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Hashable, Sequence
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import xarray as xr
|
|
15
|
+
|
|
16
|
+
from tidy3a.errors import AlignError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def align_pair(
|
|
20
|
+
left: xr.DataArray, right: xr.DataArray, left_text: str, right_text: str, notes: list[str],
|
|
21
|
+
) -> tuple[xr.DataArray, xr.DataArray]:
|
|
22
|
+
"""Return *left* and *right* ready for exact arithmetic, or raise ``AlignError``."""
|
|
23
|
+
for dim in left.dims:
|
|
24
|
+
if dim not in right.dims:
|
|
25
|
+
continue
|
|
26
|
+
right = _match_axis(
|
|
27
|
+
dim, left, right, left_text, right_text, notes,
|
|
28
|
+
context=f"in {left_text} and {right_text}",
|
|
29
|
+
)
|
|
30
|
+
return left, right
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def align_many(values: Sequence[Any], texts: Sequence[str], notes: list[str]) -> list[Any]:
|
|
34
|
+
"""Align each array to earlier operands on every shared axis; keep scalars."""
|
|
35
|
+
aligned = []
|
|
36
|
+
for value, text in zip(values, texts):
|
|
37
|
+
if isinstance(value, xr.DataArray):
|
|
38
|
+
for previous, previous_text in zip(aligned, texts):
|
|
39
|
+
if isinstance(previous, xr.DataArray):
|
|
40
|
+
_, value = align_pair(previous, value, previous_text, text, notes)
|
|
41
|
+
aligned.append(value)
|
|
42
|
+
return aligned
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def align_to(
|
|
46
|
+
value: xr.DataArray, target: xr.Dataset, text: str, notes: list[str],
|
|
47
|
+
) -> xr.DataArray:
|
|
48
|
+
"""Match *value*'s shared axes to *target*'s, reordering by label when needed."""
|
|
49
|
+
for dim in value.dims:
|
|
50
|
+
if dim not in target.dims:
|
|
51
|
+
continue
|
|
52
|
+
value = _match_axis(dim, target, value, "this data", text, notes, context=f"for {text}")
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def check_join_axes(left: xr.Dataset, right: xr.Dataset, right_text: str) -> None:
|
|
57
|
+
"""Joins align by label; a labelled axis still never pairs with an unlabelled one."""
|
|
58
|
+
for dim in left.dims:
|
|
59
|
+
if dim not in right.dims:
|
|
60
|
+
continue
|
|
61
|
+
labelled = (dim in left.indexes, dim in right.indexes)
|
|
62
|
+
if labelled[0] != labelled[1]:
|
|
63
|
+
raise AlignError(_labelled_message(dim, labelled[0], "this data", right_text))
|
|
64
|
+
if not any(labelled) and left.sizes[dim] != right.sizes[dim]:
|
|
65
|
+
raise AlignError(
|
|
66
|
+
f"{dim} is unlabelled on both sides with different lengths "
|
|
67
|
+
f"({left.sizes[dim]:,} and {right.sizes[dim]:,}); positions cannot be paired"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _match_axis(
|
|
72
|
+
dim: Hashable, left, right: xr.DataArray, left_text: str, right_text: str,
|
|
73
|
+
notes: list[str], *, context: str,
|
|
74
|
+
) -> xr.DataArray:
|
|
75
|
+
left_has = dim in left.indexes
|
|
76
|
+
right_has = dim in right.indexes
|
|
77
|
+
if left_has != right_has:
|
|
78
|
+
raise AlignError(_labelled_message(dim, left_has, left_text, right_text))
|
|
79
|
+
if not left_has:
|
|
80
|
+
if left.sizes[dim] != right.sizes[dim]:
|
|
81
|
+
raise AlignError(
|
|
82
|
+
f"{dim} has {left.sizes[dim]:,} positions in {left_text} but "
|
|
83
|
+
f"{right.sizes[dim]:,} in {right_text}, and neither has labels to match"
|
|
84
|
+
)
|
|
85
|
+
return right
|
|
86
|
+
left_index = left.indexes[dim]
|
|
87
|
+
right_index = right.indexes[dim]
|
|
88
|
+
if left_index.equals(right_index):
|
|
89
|
+
return right
|
|
90
|
+
if len(left_index) == len(right_index) and left_index.sort_values().equals(right_index.sort_values()):
|
|
91
|
+
before = " ".join(str(label) for label in _shown(right_index))
|
|
92
|
+
after = " ".join(str(label) for label in _shown(left_index))
|
|
93
|
+
notes.append(f"changed: {dim} reordered to match ({before} → {after}) {context}")
|
|
94
|
+
return right.sel({dim: left_index})
|
|
95
|
+
only_right = len(right_index.difference(left_index))
|
|
96
|
+
only_left = len(left_index.difference(right_index))
|
|
97
|
+
raise AlignError(
|
|
98
|
+
f"{dim} labels differ between {left_text} ({len(left_index):,}) "
|
|
99
|
+
f"and {right_text} ({len(right_index):,}).\n"
|
|
100
|
+
f" {only_right:,} labels are only in {right_text}; {only_left:,} only in {left_text}.\n"
|
|
101
|
+
f" To keep shared {dim} labels: inner_join(...)\n"
|
|
102
|
+
f" To keep this data's: left_join(...)"
|
|
103
|
+
+ _rounding_hint(dim, left_index, right_index)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _labelled_message(dim: Hashable, left_has: bool, left_text: str, right_text: str) -> str:
|
|
108
|
+
labelled, unlabelled = (left_text, right_text) if left_has else (right_text, left_text)
|
|
109
|
+
return (
|
|
110
|
+
f"{dim} is labelled on {labelled} but unlabelled on {unlabelled}.\n"
|
|
111
|
+
" Pairing by length could match the wrong positions.\n"
|
|
112
|
+
f' If {unlabelled} follows the same order, attach it by position: bind_cols(..., along="{dim}")'
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _rounding_hint(dim: Hashable, left_index, right_index) -> str:
|
|
117
|
+
if len(left_index) != len(right_index):
|
|
118
|
+
return ""
|
|
119
|
+
try:
|
|
120
|
+
left_values = np.asarray(left_index, dtype=float)
|
|
121
|
+
right_values = np.sort(np.asarray(right_index, dtype=float))
|
|
122
|
+
except (TypeError, ValueError):
|
|
123
|
+
return ""
|
|
124
|
+
gap = float(np.max(np.abs(np.sort(left_values) - right_values), initial=0.0))
|
|
125
|
+
scale = float(np.max(np.abs(left_values), initial=1.0)) or 1.0
|
|
126
|
+
if 0 < gap <= 1e-6 * scale:
|
|
127
|
+
return (
|
|
128
|
+
f"\n The labels differ only by up to {gap:.2g} (rounding).\n"
|
|
129
|
+
" To match them within a tolerance: left_join(..., tolerance=1e-9)"
|
|
130
|
+
)
|
|
131
|
+
return ""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _shown(index) -> list:
|
|
135
|
+
values = list(index)
|
|
136
|
+
return values if len(values) <= 4 else [*values[:3], "…"]
|
tidy3a/errors.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Exceptions raised by tidy3A.
|
|
2
|
+
|
|
3
|
+
Every tidy3A error is a ``ValueError``, so code that already catches bad
|
|
4
|
+
input keeps working; the subclasses say which step rejected it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TidyError(ValueError):
|
|
9
|
+
"""Base class for tidy3A errors."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ReadError(TidyError):
|
|
13
|
+
"""An array or record reader cannot read the source as described."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RefError(TidyError):
|
|
17
|
+
"""A name in an expression is unknown or means more than one thing."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SelectError(RefError):
|
|
21
|
+
"""``select`` cannot pick what was asked for."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FilterError(TidyError):
|
|
25
|
+
"""``filter`` cannot keep a rectangle of positions."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SliceError(TidyError):
|
|
29
|
+
"""``slice`` was given positions that do not exist."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MutateError(TidyError):
|
|
33
|
+
"""``mutate`` cannot store a result."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GroupError(TidyError):
|
|
37
|
+
"""``group_by`` cannot group by what was asked for."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SummariseError(TidyError):
|
|
41
|
+
"""``summarise`` cannot reduce a summary to one value per group."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AlignError(TidyError):
|
|
45
|
+
"""Labels on a shared axis do not match."""
|